Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

std::move_if_noexcept calls copy-assignment even though move-assignment is noexcept; why?

Tags:

c++

c++14

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"
}

The Question

  • Why isn't the move-assignment operator called in the previous snippet?
like image 991
Filip Roséen - refp Avatar asked Feb 19 '15 04:02

Filip Roséen - refp


People also ask

Does move assignment call destructor?

Default move assignment calls destructor, copy assignment doesn't.

When move constructor is not generated?

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.

Are move constructor automatically generated?

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.

Is the default move constructor Noexcept?

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 ...


1 Answers

The Introduction

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:

  1. A simple cast from T& to T&& or T const& can never throw an exception, so what is the purpose of such function?
  2. How can 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.


The TL;DR

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.


The Birth Of 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 use std::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 copying x, just as though x had never acquired a move constructor at all.


So, what is it that move_if_noexcept does?

std::move_if_noexcept will conditionally cast the passed lvalue-reference to an rvalue-reference, unless;

  • A potential move-constructor might throw, and;
  • the type is CopyConstructible.
// 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).


The Verdict

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 { /* ... */ }
  
  ...
};

Examples

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&&`

Further reading:

  • cppreference.com - MoveConstructible
  • cppreference.com - CopyConstructible
  • cppreference.com - std::move_if_noexcept
like image 108
Filip Roséen - refp Avatar answered Oct 13 '22 11:10

Filip Roséen - refp