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

Dan*_*ica 21 c++ language-lawyer move-semantics copy-elision perfect-forwarding

我无法弄清楚为什么在最后一种情况下启用了复制省略时调用移动构造函数(甚至是强制性的,例如在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
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,哪些规则阻碍了移动构造函数被省略?

UPDATE

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

X x4 = std::forward<X>(X(1));
X x5 = static_cast<X&&>(X(1));
Run Code Online (Sandbox Code Playgroud)

Bar*_*rry 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));
}
Run Code Online (Sandbox Code Playgroud)

因为x1,我们所拥有的第一个表达式X是身体中的一个,make_X基本上就是return X(1).这是一种类型的prvalue X.我们正在make_X使用该prvalue 初始化返回对象,然后make_X(1)它本身就是一个类型的prvalue X,所以我们推迟实现.T从类型的prvalue 初始化类型的对象T意味着直接从初始化器初始化,因此auto x1 = make_X(1)简化为just X x1(1).

因为x2,减少甚至更简单,我们只是直接应用规则.

因为x3,情况不同.我们有一个X 早期类型的prvalue (X(1)参数),并且prvalue绑定到引用!在绑定点,我们应用临时实现转换 - 这意味着我们实际上创建了一个临时对象.则对象被移入返回的对象,我们可以在随后的表达做prvalue减少一路.所以这基本上减少了:

X __tmp(1);
X x3(std::move(__tmp));
Run Code Online (Sandbox Code Playgroud)

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


对于最后两种情况:

X x4 = std::forward<X>(X(1));
X x5 = static_cast<X&&>(X(1));
Run Code Online (Sandbox Code Playgroud)

在这两种情况下,我们都绑定了对prvalue的引用,这又需要临时实现转换.然后在这两种情况下,初始化器都是一个xvalue,所以我们没有得到prvalue减少 - 我们只是从xvalue移动构造,它是一个物化的临时对象来自prvalue.


Oli*_*liv 5

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

auto x4 = make_X(std::move(x2));
Run Code Online (Sandbox Code Playgroud)

x4会成为别名x2.

返回值的移动省略规则在[class.copy]/32中描述:

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

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

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

在调用make_X(X(1))副本elision实际发生,但只有一次:

  1. 首先X(1)创建一个绑定的临时arg.
  2. 然后X(std::forward<T>(arg))调用move构造函数.arg不是暂时的,所以上面的第二条规则不适用.
  3. 然后结果表达式X(std::forward<T>(arg))也应该被移动以构造返回值,但是这个移动被省略了.

关于你的UPDATE,std::forward导致X(1)绑定到xvalue 的临时实现:返回std::forward.此返回的xvalue不是临时的,因此复制/省略不再适用.

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

auto x7 = std::forward<X>(std::move(x2));
Run Code Online (Sandbox Code Playgroud)

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

在C++ 17中,prvalue更改的定义是,在示例代码中不再有任何移动构造函数.这里是GCC 的结果代码示例,带有fno-elide-constructorsC++ 14中的选项,然后是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
Run Code Online (Sandbox Code Playgroud)