C++ std::move_if_noexcept调用复制分配,即使move分配是noexcept;为什么?

C++ std::move_if_noexcept调用复制分配,即使move分配是noexcept;为什么?,c++,c++14,C++,C++14,我试图尽可能接近强异常保证,但在使用std::move_if_noexcept时,我遇到了一些看似奇怪的行为 尽管以下类中的移动赋值运算符标记为noexcept,但当使用相关函数的返回值调用时,将调用复制赋值运算符 struct A { A () { /* ... */ } A (A const&) { /* ... */ } A& operator= (A const&) noexcept { log ("copy-assign&

我试图尽可能接近强异常保证,但在使用
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";
  }
};

问题
  • 为什么在前面的代码段中没有调用move赋值操作符
导言
move\u if\u noexcept
的名称当然意味着,只要此操作是
noexcept
,函数就会产生一个右值引用,记住这一点,我们很快就会意识到两件事:

  • T&
    T&
    T const&
    的简单强制转换永远不会引发异常,那么该函数的用途是什么
  • 如果没有异常,
    move\u如何神奇地推断出使用返回值的上下文
  • 实现的答案(2)是同样可怕的自然
    move\u if\u noexcept
    根本无法推断出这样的上下文(因为它不是读心器),这反过来意味着该函数必须按照一组静态规则运行


    TL;博士
    move\u if\u noexcept
    将根据参数类型的move-构造函数的异常规范,有条件地返回右值引用,并且该引用仅用于初始化对象时(即不用于分配对象时)

    这基本上意味着,只有当它能够证明它是唯一可行的替代方案,或者如果它保证不会抛出异常(通过
    noexcept
    表示),它才会产生一个右值引用


    判决书
    std::move
    是对右值引用的无条件强制转换,而
    std::move_if_noexcept
    取决于对象的移动构造方式-因此,它应该只在我们实际构造对象的地方使用,而不是在分配对象时使用

    template<class T>
    void intended_usage () {
      T first;
      T second (std::move_if_noexcept (first));
    }
    
    如果noexcept
    找不到标记为
    noexcept
    的移动构造函数,将调用代码段中的复制赋值运算符,但由于它有一个复制构造函数,该函数将生成一个类型,
    一个常量&
    ,该类型适用于这种情况


    请注意,复制构造函数符合可移动构造函数的类型,这意味着我们可以通过对代码段进行以下调整,使
    move\u if\u noexcept
    返回右值引用:

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

    例子
    进一步阅读:

    这个问题是由我的一位学生提出的,我希望我能公正地回答最初的问题(即使是通过这个极其简化的版本)。因此,这实际上是一个非常有趣的微妙之处,我非常感谢你的分析,但我认为你在回答中遗漏了一个有趣的例子。您正确地观察到,
    std::move_if_noexcept
    的行为似乎取决于移动构造,而不是赋值,但在问题提示中,由于特殊的函数生成规则,您根本没有移动构造函数,更不用说移动构造函数了。如果我添加一个,那么它确实会打印出“move assign”。(请参阅:)虽然我不太喜欢假设,但我认为,如果有人费心编写
    noexcept
    move构造函数,那么他们的move赋值操作符也是
    noexcept
    ,这是半合理的假设。因此,我希望
    std::move_如果没有异常
    在99.9%的时间里做了预期的事情。当然,我以前在行业中见过0.1%的案例,所以,这一发现值得称赞!关于提交缺陷报告的第一步:。很抱歉有这么多背对背的注释。@标记:复制构造函数符合使对象可移动可构造的条件,并且由于声明的函数未标记为noexcept
    is_nothrow\u move\u constructible::value==false
    ;我将把这一点添加到答案中,以澄清问题。
    // 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;
    
    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&&`