std :: move_if_noexcept调用copy-assignment,即使move-assignment是noexcept; 为什么?

Fil*_*efp 16 c++ c++14

我试图尽可能接近强异常保证,但在玩的时候std::move_if_noexcept我遇到了一些看似奇怪的行为.

尽管在布展赋值运算符在下面的类标记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";
  }
};
Run Code Online (Sandbox Code Playgroud)
int main () {
  A x, y;

  x = std::move_if_noexcept (y); // prints "copy-assign"
}
Run Code Online (Sandbox Code Playgroud)

问题

  • 为什么在前一个代码段中没有调用move-assignment运算符

Fil*_*efp 15

简介

这个名称move_if_noexcept肯定意味着只要这个操作,函数就会产生一个rvalue-referencenoexcept,并且考虑到这一点,我们很快就会意识到两件事:

  1. 一个简单的强制T&转换,T&& 或者T const&永远不会抛出异常,那么这个函数的目的是什么?
  2. 如何move_if_noexcept神奇地推断出使用返回值的上下文?

实现(2)的答案与自然同样可怕; move_if_noexcept根本不能推断出这样的上下文(因为它不是一个心灵读者),这反过来意味着该函数必须通过一些静态规则集来发挥作用.


TL; DR

move_if_noexceptwill,无论调用它的上下文,有条件地返回一个rvalue-reference,具体取决于参数类型的move- 构造函数的异常规范,并且它只是在初始化对象时使用(即,不是在分配它们时) ).

template<class T>
void intended_usage () {
  T first;
  T second (std::move_if_noexcept (first));
}
Run Code Online (Sandbox Code Playgroud)

可能是一个更好的名字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将有条件地将传递的左值引用转换为右值引用,除非;

// 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;
Run Code Online (Sandbox Code Playgroud)

这基本上意味着它只会产生一个rvalue-reference,如果它能证明它是唯一可行的替代方案,或者保证不抛出异常(通过表示noexcept).


判决

std::move是对rvalue-reference的无条件转换,而std::move_if_noexcept取决于对象可以移动构造的方式 - 因此它只应该用于我们实际构建对象的地方,而不是在我们分配它们时.

您的代码段中的复制赋值运算符被调用,因为move_if_noexcept无法找到标记的移动构造函数noexcept,但由于它具有复制构造函数,因此该函数将生成A const&适合此类型的类型.


请注意,复制构造函数符合MoveConstructible类型,这意味着我们可以通过以下调整您的代码段move_if_noexcept返回rvalue-reference:

struct A {
  A ()                  { /* ... */ }
  A (A const&) noexcept { /* ... */ }

  ...
};
Run Code Online (Sandbox Code Playgroud)

例子

struct A {
  A ();
  A (A const&);
};

A a1;
A a2 (std::move_if_noexcept (a1)); // `A const&` => copy-constructor
Run Code Online (Sandbox Code Playgroud)
struct B {
  B ();
  B (B const&);
  B (B&&) noexcept;
};

B b1;
B b2 (std::move_if_noexcept (b1)); // `B&&` => move-constructor
                                   //          ^ it's `noexcept`
Run Code Online (Sandbox Code Playgroud)
struct C {
  C ();
  C (C&&);
};

C c1;
C c2 (std::move_if_noexcept (c1)); // `C&&` => move-constructor
                                   //          ^ the only viable alternative
Run Code Online (Sandbox Code Playgroud)
struct D {
  C ();
  C (C const&) noexcept;
};

C c1;
C c2 (std::move_if_noexcept (c1)); // C&& => copy-constructor
                                   //        ^ can be invoked with `T&&`
Run Code Online (Sandbox Code Playgroud)

进一步阅读: