Std:: move_if_noexcept вызывает назначение копии, хотя присваивание переходов не является исключением; Зачем?

Я пытаюсь как можно ближе подойти к гарантии Strong Exception, но, играя с std::move_if_noexcept, я столкнулся с каким-то странным поведением.

Несмотря на то, что оператор move-assign в следующем классе помечен как noexcept, оператор присваивания копии вызывается при вызове с возвращаемым значением рассматриваемой функции.

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

Вопрос

  • Почему оператор переноса не вызван в предыдущем фрагменте?

Ответ 1

Введение

Название move_if_noexcept, безусловно, означает, что функция будет давать ссылку rvalue, если эта операция noexcept, и имея в виду это, мы вскоре понимаем две вещи:

  • Простой способ от T& до T&& или T const& никогда не может генерировать исключение, поэтому в чем цель такой функции?
  • Как move_if_noexcept волшебным образом вывести контекст, в котором будет использоваться возвращаемое значение?

Ответ на реализацию (2) одинаково страшен, как естественный; move_if_noexcept просто не может вывести такой контекст (поскольку он не является читателем-читателем), а это, в свою очередь, означает, что функция должна играть какой-то статический набор правил.


TL; DR

move_if_noexcept будет независимо от контекста, в котором он вызван, условно возвращает ссылку rvalue в зависимости от спецификации исключения типа move move конструктора, и это было предназначено только для используется при инициализации объектов (т.е. не при назначении им).

template<class T>
void intended_usage () {
  T first;
  T second (std::move_if_noexcept (first));
}

Лучшее имя могло бы быть move_if_move_ctor_is_noexcept_or_the_only_option; хотя и немного утомительно печатать, по крайней мере, он выразил бы предполагаемое использование.


Рождение move_if_noexcept

Считая предложение (n3050), которое родило std::move_if_noexcept, мы находим следующий абзац (подчеркните мой):

Мы предполагаем, что вместо использования std::move(x) в этих случаях, предоставляя разрешение компилятору использовать любой доступный конструктор перемещения , разработчики этих конкретных операций должны использовать std::move_if_noexcept(x), который предоставляет разрешение переместить, если он не может выбраться, и тип может быть скопирован.

Если x - тип только для перемещения или, как известно, имеет конструктор недропользователя , операция вернется к копированию x, как если бы x никогда не приобрел конструктор перемещения .


Итак, что делает move_if_noexcept?

std::move_if_noexcept будет условно передавать переданную ссылку lvalue-ссылке на rvalue-reference, если:

  • Может быть создан потенциальный конструктор move и
  • тип 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;

В основном это означает, что он даст только rvalue-reference, если он может доказать, что он является единственной жизнеспособной альтернативой, или если он не будет генерировать исключение (выраженное через noexcept).


Вердикт

std::move - это безусловный приведение в rvalue-reference, тогда как std::move_if_noexcept зависит от способов построения объекта - поэтому его следует использовать только в тех местах, где мы строим объекты, а не когда мы назначаются им.

Вызывается оператор присваивания копий в вашем фрагменте, так как move_if_noexcept не может найти конструктор move, помеченный noexcept, но поскольку он имеет конструктор-копию, функция будет выдавать тип A const&, который подходит для таких целей.


Обратите внимание, что конструктор-копир квалифицируется как тип MoveConstructible, это означает, что мы можем сделать move_if_noexcept вернуть ссылку rvalue через следующую настройку вашего фрагмента:

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

Дальнейшее чтение: