I'm trying to get as close to the Strong Exception Guarantee as possible, but when playing around with std::move_if_noexcept
I ran into some seemingly weird behavior.
Despite the fact that the move-assignment operator in the following class is marked noexcept
, the copy-assignment operator is called when invoked with the return value of the function in question.
struct A {
A () { /* ... */ }
A (A const&) { /* ... */ }
A& operator= (A const&) noexcept { log ("copy-assign"); return *this; }
A& operator= (A&&) noexcept { log ("move-assign"); return *this; }
static void log (char const * msg) {
std::cerr << msg << "\n";
}
};
int main () {
A x, y;
x = std::move_if_noexcept (y); // prints "copy-assign"
}
Default move assignment calls destructor, copy assignment doesn't.
The move constructor is not generated because you declared a copy constructor. Remove the private copy constructor and copy assignment. Adding a non-copyable member (like a unique_ptr ) already prevents generation of the copy special members, so there's no need to prevent them manually, anyway.
If a copy constructor, copy-assignment operator, move constructor, move-assignment operator, or destructor is explicitly declared, then: No move constructor is automatically generated. No move-assignment operator is automatically generated.
Inheriting constructors and the implicitly-declared default constructors, copy constructors, move constructors, destructors, copy-assignment operators, move-assignment operators are all noexcept(true) by default, unless they are required to call a function that is noexcept(false) , in which case these functions are ...
The name of move_if_noexcept
certainly implies that the function will yield an rvalue-reference as long as this operation is noexcept
, and with this in mind we soon realize two things:
T&
to T&&
or T const&
can never throw an exception, so what is the purpose of such function?move_if_noexcept
magically deduce the context in which the returned value will be used?The answer to realization (2) is equally scary as natural; move_if_noexcept
simply can't deduce such context (since it's not a mind-reader), and this in turn means that the function must play by some static set of rules.
move_if_noexcept
will, no matter the context in which it is called, conditionally return an rvalue-reference depending on the exception specification of the argument type's move-constructor, and it was only meant to be used when initializing objects (ie. not when assigning to them).
template<class T>
void intended_usage () {
T first;
T second (std::move_if_noexcept (first));
}
A better name could have been move_if_move_ctor_is_noexcept_or_the_only_option
; though a bit tedious to type, at least it would have expressed the intended usage.
move_if_noexcept
Reading the proposal (n3050) that gave birth to std::move_if_noexcept
, we find the following paragraph (emphasize mine):
We propose that instead of using
std::move(x)
in those cases, thus granting permission for the compiler to use any available move constructor, maintainers of these particular operations should usestd::move_if_noexcept(x)
, which grants permission move unless it could throw and the type is copyable.Unless
x
is a move-only type, or is known to have a nonthrowing move constructor, the operation would fall back to copyingx
, just as thoughx
had never acquired a move constructor at all.
move_if_noexcept
does?std::move_if_noexcept
will conditionally cast the passed lvalue-reference to an rvalue-reference, unless;
// Standard Draft n4140 : [utility]p2
template<class T>
constexpr conditional_t<
!is_nothrow_move_constructible::value && is_copy_constructible<T>::value,
const T&, T&&
> move_if_noexcept (T& x) noexcept;
This basically means that it will only yield an rvalue-reference if it can prove that it is the only viable alternative, or if it is guaranteed not to throw an exception (expressed through noexcept
).
std::move
is an unconditional cast to an rvalue-reference, whereas std::move_if_noexcept
depends on the ways an object can be move-constructed - therefore it should only be used in places where we are actuallying constructing objects, not when we are assigning to them.
The copy-assignment operator in your snippet is invoked since move_if_noexcept
can't find a move-constructor marked noexcept
, but since it has a copy-constructor the function will yield a type, A const&
, that is suitable for such.
Please note that a copy-constructor qualifies as the type being MoveConstructible, this means that we can make move_if_noexcept
return an rvalue-reference through the following adjustment of your snippet:
struct A {
A () { /* ... */ }
A (A const&) noexcept { /* ... */ }
...
};
struct A {
A ();
A (A const&);
};
A a1;
A a2 (std::move_if_noexcept (a1)); // `A const&` => copy-constructor
struct B {
B ();
B (B const&);
B (B&&) noexcept;
};
B b1;
B b2 (std::move_if_noexcept (b1)); // `B&&` => move-constructor
// ^ it's `noexcept`
struct C {
C ();
C (C&&);
};
C c1;
C c2 (std::move_if_noexcept (c1)); // `C&&` => move-constructor
// ^ the only viable alternative
struct D {
C ();
C (C const&) noexcept;
};
C c1;
C c2 (std::move_if_noexcept (c1)); // C&& => copy-constructor
// ^ can be invoked with `T&&`
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With