【问题标题】:Why isn't move constructor elided whenever possible with `make_x()` functions?为什么不使用 `make_x()` 函数尽可能省略移动构造函数?
【发布时间】:2018-06-21 21:57:48
【问题描述】:

我不明白为什么在最后一种情况下是 在启用复制省略时调用移动构造函数(甚至是强制性的,例如在 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));

【问题讨论】:

  • 你认为你的移动构造函数什么时候被调用?
  • @Pumkko 输出确实显示了移动构造函数调用cpp.sh/8lkmo
  • 您是否仔细检查过您的编译器供应商可能对这个主题所说的话?我怀疑一些编译器可能会采用不同的方法来避免破坏遗留代码。
  • @SirGuy 我认为编译器在 C++17 中没有任何选项。我已经用-std=c++1z 尝试了 GCC 和 Clang。 (例如,x1x2 的初始化在 C++17 中是正确的,即使 X 的复制和移动 ctor 都被删除。)
  • 查看禁用复制省略的输出coliru.stacked-crooked.com/a/efd8922fec01bafc

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


【解决方案1】:

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

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));
}

对于x1X 类型的第一个表达式是make_X 的主体中的表达式,基本上是return X(1)。这是X 类型的prvalue。我们用那个纯右值初始化make_X 的返回对象,然后make_X(1) 本身就是一个X 类型的纯右值,所以我们延迟了实现。从T 类型的纯右值初始化T 类型的对象意味着直接初始化from the initializer,所以auto x1 = make_X(1) 减少到只是X x1(1)

对于x2,归约更简单,直接套用规则即可。

对于x3,情况有所不同。我们有一个 Xearlier 类型的纯右值(X(1) 参数)并且该纯右值绑定到一个引用!在绑定点,我们应用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));

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

【讨论】:

  • 注意我听说过一个提议(当不存在时)大大扩展省略权限以包括(除其他外)上述情况。我没有链接或号码。
  • ... 你能解释一下为什么绑定到右值引用,然后是移动构造,与从纯右值构造的纯右值构造不同吗?我的意思是,为什么第二个被认为值得省略而另一个不值得? @Yakk 如果您愿意,也请您对此发表评论。
  • @einpoklum 因为标准允许您在狭窄的环境中回避,这不是其中之一吗?右值引用不是纯右值。 prvalue 是一种右值。从纯右值、NRVO 和一些基于异常的情况构造对象时,可能会发生省略。
  • 是的,你的第一句话听起来不错。 Antony Polukhin 也一直在说同样的话,并试图推动一项扩大情况的提议。
【解决方案2】:

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

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

x4 将成为x2 的别名。

返回值移动省略的规则见[class.copy]/32:

[...]这种复制/移动操作的省略,称为复制省略,在以下情况下是允许的(可以结合起来消除多个副本):

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

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

在调用make_X(X(1)) 复制省略实际上发生了,但只有一次:

  1. 第一个 X(1) 创建一个绑定到 arg 的临时对象。
  2. 然后X(std::forward&lt;T&gt;(arg)) 调用移动构造函数。 arg 不是临时的,所以上面的第二条规则不适用。
  3. 那么表达式X(std::forward&lt;T&gt;(arg)) 的结果也应该被移动来构造返回值,但是这个移动被省略了。

关于您的更新,std::forward 导致绑定到 xvalue 的临时 X(1) 的具体化:std::forward 的返回。这个返回的 xvalue 不是临时的,所以复制/省略不再适用。

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

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

注意:在我看到一个关于 C++17 的新答案后,我想增加困惑。

在 C++17 中,prvalue 的定义改变为不再有任何移动构造函数可以在示例代码中省略。这里是 GCC 的 result code 示例,在 C++14 和 C++17 中带有选项 fno-elide-constructors

#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

【讨论】:

  • IANALL,但仍然:“绑定到临时的引用......仍然不是临时的”x(x(1)) 的情况下,如果没有省略,我们应该为第二个x 调用复制ctor。 “在函数体内,编译器无法确保 arg 未绑定到左值。”
【解决方案3】:

为了简化您的示例:

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 起)的构造 类对象...

  • 如果函数按值返回类类型,则返回 语句的表达式是一个非易失性对象的名称 自动存储持续时间,它不是函数参数,也不是 catch 子句参数,并且具有相同的类型(忽略 顶级 cv-qualification) 作为函数的返回类型,然后 复制/移动 (C++11 起) 被省略。当该本地对象是 构造,它直接在存储中构造 否则函数的返回值将被移动或复制到。这 复制省略的变体称为 NRVO,“命名返回值 优化”。

c++17 起:

以下情况,编译器需要省略 复制和移动构造...

a) 在初始化中,如果初始化表达式是纯右值并且 源类型的 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:它不按值返回类类型。因此不会发生省略。

【讨论】:

  • 这就是为什么auto x4 = X(std::move(X(1))); 也会导致“转换”和“移动”。
猜你喜欢
  • 2023-03-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-06-10
相关资源
最近更新 更多