From 856c3352a77c2021a73ef2e8fb8de10ae60b1b86 Mon Sep 17 00:00:00 2001 From: Christopher Albert Date: Tue, 28 Apr 2026 20:37:57 +0200 Subject: [PATCH] [handle] guard self-assign on owning-handle assignments Move-assigns (handle_heap, handle_stack, handle_sso): add EXPECTS(this != &h). EXPECTS is a no-op under NDEBUG, so release builds pay nothing on the hot path. Copy-assigns (handle_heap, handle_stack): add the standard fast-path if (this == &h) return *this; (handle_sso already has it). handle_borrowed is = default and unaffected. check_handle() in nda_mem.cpp deliberately exercised self-swap and self-move-assign, both now precondition violations. Gated under #ifdef NDEBUG so release-mode coverage is preserved. Closes #11. --- c++/nda/mem/handle.hpp | 12 +++++++++--- test/c++/nda_mem.cpp | 7 +++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/c++/nda/mem/handle.hpp b/c++/nda/mem/handle.hpp index bf3ed597c..084acdab0 100644 --- a/c++/nda/mem/handle.hpp +++ b/c++/nda/mem/handle.hpp @@ -169,9 +169,11 @@ namespace nda::mem { * @brief Move assignment operator first releases the resources held by the current handle and then moves the * resources from the source to the current handle. * - * @param h Source handle. + * @param h Source handle. Must not alias `*this`. */ handle_heap &operator=(handle_heap &&h) noexcept { + EXPECTS(this != &h); + // release current resources if they are not shared and not null if (not sptr and not(is_null())) destruct({_data, _size}); @@ -206,6 +208,7 @@ namespace nda::mem { * @param h Source handle. */ handle_heap &operator=(handle_heap const &h) { + if (this == &h) return *this; *this = handle_heap{h}; return *this; } @@ -382,9 +385,10 @@ namespace nda::mem { /** * @brief Move assignment operator simply calls the copy assignment operator. * @details If an exception occurs in the constructor of `T`, the program terminates. - * @param h Source handle. + * @param h Source handle. Must not alias `*this`. */ handle_stack &operator=(handle_stack &&h) noexcept { + EXPECTS(this != &h); operator=(h); return *this; } @@ -401,6 +405,7 @@ namespace nda::mem { * @param h Source handle. */ handle_stack &operator=(handle_stack const &h) { + if (this == &h) return *this; for (size_t i = 0; i < Size; ++i) new (data() + i) T(h[i]); return *this; } @@ -545,9 +550,10 @@ namespace nda::mem { * * @details In both cases, it resets the source handle to a null state. * - * @param h Source handle. + * @param h Source handle. Must not alias `*this`. */ handle_sso &operator=(handle_sso &&h) noexcept { + EXPECTS(this != &h); clean(); _size = h._size; if (on_heap()) { diff --git a/test/c++/nda_mem.cpp b/test/c++/nda_mem.cpp index 9013deedf..35f12a8ca 100644 --- a/test/c++/nda_mem.cpp +++ b/test/c++/nda_mem.cpp @@ -121,18 +121,21 @@ H check_handle() { move2 = std::move(copy1); EXPECT_EQ(handle.size(), move1.size()); EXPECT_EQ(handle.size(), move2.size()); - std::swap(handle, handle); // check self swap +#ifdef NDEBUG // self-swap violates the handle move-assign precondition; release-only. + std::swap(handle, handle); +#endif for (int i = 0; i < handle.size(); ++i) { EXPECT_EQ(handle[i], static_cast(i)); EXPECT_EQ(move1[i], static_cast(i)); EXPECT_EQ(move2[i], static_cast(i)); } - // check self move assignment (see https://stackoverflow.com/questions/9322174/move-assignment-operator-and-if-this-rhs) +#ifdef NDEBUG // self-move-assign violates the handle move-assign precondition; release-only. #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wself-move" move1 = std::move(move1); #pragma GCC diagnostic pop +#endif return handle; }