为什么不能使用`make_x()`函数移动构造函数?

时间:2018-01-12 12:23:40

标签: c++ language-lawyer move-semantics perfect-forwarding copy-elision

我无法弄清楚为什么在最后一种情况下启用了复制省略时调用的移动构造函数(甚至是强制性的,例如在C ++ 17中):

class X {
  public:
    X(int i) { std::clog << "converting\n"; }
    X(const X &) { std::clog << "copy\n"; }
    X(X &&) { std::clog << "move\n"; }
};

template <typename T>
X make_X(T&& arg) {
  return X(std::forward<T>(arg));
}

int main() {
  auto x1 = make_X(1);    // 1x converting ctor invoked
  auto x2 = X(X(1));      // 1x converting ctor invoked
  auto x3 = make_X(X(1)); // 1x converting and 1x move ctor invoked
}

在这种情况下,哪些规则会妨碍移动构造函数被忽略?

更新

调用移动构造函数时可能会有更直接的情况:

X x4 = std::forward<X>(X(1));
X x5 = static_cast<X&&>(X(1));

3 个答案:

答案 0 :(得分:17)

这两种情况略有不同,理解其中的原因非常重要。使用C ++ 17中的新值语义,基本思想是我们尽可能地延迟将prvalues转换为对象的过程。

template <typename T>
X make_X(T&& arg) {
  return X(std::forward<T>(arg));
}

int main() {
  auto x1 = make_X(1);
  auto x2 = X(X(1));
  auto x3 = make_X(X(1));
}

对于x1,我们第一个类型X的表达式是make_X正文中的表达式,基本上是return X(1)。这是X类型的prvalue。我们正在使用该prvalue初始化make_X的返回对象,然后make_X(1)本身就是X类型的prvalue,因此我们将延迟实现。从T类型的prvalue初始化T类型的对象意味着直接初始化from the initializer,因此auto x1 = make_X(1)缩减为X x1(1)

对于x2,减少甚至更简单,我们只是直接应用规则。

对于x3,情况不同。我们有一个类型X 早期X(1)参数)的prvalue,并且prvalue绑定到引用!在绑定时,我们应用the temporary materialization conversion - 这意味着我们实际创建a temporary object然后将对象移动到返回对象中,我们可以一直对后续表达式进行prvalue减少。所以这基本上减少了:

X __tmp(1);
X x3(std::move(__tmp));

我们仍有一个动作,但只有一个动作(我们可以避免链式动作)。它是对引用的绑定,需要存在单独的X对象。参数argmake_X的返回对象必须是不同的对象 - 这意味着必须进行移动。

最后两个案例:

X x4 = std::forward<X>(X(1));
X x5 = static_cast<X&&>(X(1));

在这两种情况下,我们都会绑定对prvalue的引用,这又需要进行临时实现转换。然后在这两种情况下,初始化器都是x值,所以我们不能减少prvalue - 我们只是从xvalue移动构造,它是来自prvalue的物化临时对象。

答案 1 :(得分:5)

因为在表达式X(std::forward<T>(arg))中,即使在最后一种情况下,arg是绑定到临时的引用,它仍然不是临时的。在函数体内,编译器无法确保arg未绑定到左值。考虑如果移动构造函数被删除会发生什么,并且您将执行此调用:

auto x4 = make_X(std::move(x2));

x4将成为x2的别名。

[class.copy]/32

中描述了返回值的移动省略规则
  

[...]在下列情况下(可以合并以消除多份副本),允许复制/移动操作的省略,称为复制省略:

     
      
  • 在具有类返回类型的函数的return语句中,当表达式是具有相同cv-unqualified类型的非易失性自动对象(函数或catch子句参数除外)的名称时作为函数返回类型,可以通过将自动对象直接构造为函数的返回值来省略复制/移动操作

  •   
  • 当一个未绑定到引用([class.temporary])的临时类对象被复制/移动到具有相同cv-nonqualified类型的类对象时,复制/移动操作可以是通过将临时对象直接构造到省略的副本/移动

  • 的目标中而省略   

在电话make_X(X(1))中,复制省略实际发生,但只发生一次:

  1. 首先X(1)创建一个绑定到arg的临时文件。
  2. 然后X(std::forward<T>(arg))调用移动构造函数。 arg不是临时的,因此上述第二条规则不适用。
  3. 然后结果表达式X(std::forward<T>(arg))也应该被移动以构造返回值,但是这个移动被省略了。
  4. 关于您的更新,std::forward导致绑定到xvalue的临时X(1)的实现:std::forward的返回。此返回的xvalue不是临时的,因此复制/省略不再适用。

    如果发生移动省略,在这种情况下会发生什么。 (c ++语法不是上下文):

    auto x7 = std::forward<X>(std::move(x2));
    

    Nota:在我看到关于C ++ 17的新答案后,我想加入混乱。

    在C ++ 17中,prvalue的定义发生了变化,即在示例代码中没有任何移动构造函数可以删除。这里是GCC的result code示例,在C ++ 14中使用选项fno-elide-constructors,然后在C ++ 17中:

    #c++ -std=c++14 -fno-elide-constructors | #c++ -std=c++17 -fno-elide-constructors
    main:                                   | main:
      sub rsp, 24                           |   sub rsp, 24
      mov esi, 1                            |   mov esi, 1
      lea rdi, [rsp+15]                     |   lea rdi, [rsp+12]
      call X::X(int)                        |   call X::X(int)
      lea rsi, [rsp+15]                     |   lea rdi, [rsp+13]
      lea rdi, [rsp+14]                     |   mov esi, 1
      call X::X(X&&)                        |   call X::X(int)
      lea rsi, [rsp+14]                     |   lea rdi, [rsp+15]
      lea rdi, [rsp+11]                     |   mov esi, 1
      call X::X(X&&)                        |   call X::X(int)
      lea rdi, [rsp+14]                     |   lea rsi, [rsp+15]
      mov esi, 1                            |   lea rdi, [rsp+14]
      call X::X(int)                        |   call X::X(X&&)
      lea rsi, [rsp+14]                     |   xor eax, eax
      lea rdi, [rsp+15]                     |   add rsp, 24
      call X::X(X&&)                        |   ret               
      lea rsi, [rsp+15]
      lea rdi, [rsp+12]
      call X::X(X&&)
      lea rdi, [rsp+13]
      mov esi, 1
      call X::X(int)
      lea rsi, [rsp+13]
      lea rdi, [rsp+15]
      call X::X(X&&)
      lea rsi, [rsp+15]
      lea rdi, [rsp+14]
      call X::X(X&&)
      lea rsi, [rsp+14]
      lea rdi, [rsp+15]
      call X::X(X&&)
      xor eax, eax
      add rsp, 24
      ret
    

答案 2 :(得分:4)

简化您的示例:

auto x1 = make_X(1);                // converting
auto x2 = X(X(1));                  // converting
auto x4 = X(std::forward<X>(X(1))); // converting + move

来自cppreference的copy elision documentation(强调我的):

在c ++ 17之前:

  

在下列情况下,编译器是允许的,但是   不要求省略复制和移动 - (自C ++ 11)构造   类对象......

     
      
  • 如果函数按值返回类类型,则返回   statement&table;表达式是非易失性对象的名称   自动存储持续时间,不是功能参数,或者是   catch子句参数,它具有相同的类型(忽略   顶级cv-qualification)作为函数的返回类型,然后   复制/移动(因为C ++ 11)被省略。当那个本地对象是   建造后,它直接在仓库中建造   否则,函数的返回值将被移动或复制到。这个   复制省略的变体称为NRVO,&#34;命名为返回值   优化&#34;
  •   

从c ++ 17开始:

  

在下列情况下,编制者需要省略   复制和移动构建......

     

a)在初始化中,如果初始化表达式是prvalue而且是   cv-源类型的非限定版本与该类相同   目的地的类,初始化表达式用于   初始化目标对象:

T x = T(T(T())); // only one call to default constructor of T, to initialize x
     

b)在函数调用中,如果 return语句的操作数是 prvalue   并且函数的返回类型与其类型相同   prvalue。

T f() { return T{}; }
T x = f();         // only one call to default constructor of T, to initialize x
T* p = new T(f()); // only one call to default constructor of T, to initialize *p

在任何情况下std::forward都不符合要求,因为它的结果是 xvalue ,而不是 prvalue :它不会返回类按值键入。因此,不会出现任何缺陷。