为什么尽可能不使用"make_x()"函数省略移动构造函数?
Why isn't move constructor elided whenever possible with `make_x()` functions?
我不知道为什么在最后一种情况下在启用复制 elision 时调用移动构造函数(甚至是强制性的,例如 C++17):
class X {
public:
X(int i) { std::clog << "convertingn"; }
X(const X &) { std::clog << "copyn"; }
X(X &&) { std::clog << "moven"; }
};
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));
这两种情况略有不同,了解原因很重要。对于 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 初始化make_X
的返回对象,然后make_X(1)
本身是X
类型的 prvalue,因此我们延迟了具体化。从T
类型的 prvalue 初始化T
类型的对象意味着直接从初始值设定项初始化,因此auto x1 = make_X(1)
减少到只有X x1(1)
。
对于x2
,减少更简单,我们只是直接应用规则。
对于x3
,情况是不同的。我们之前有一个X
类型的 prvalue(X(1)
参数),并且该 prvalue 绑定到引用!在绑定点,我们应用临时物化转换 - 这意味着我们实际上创建了一个临时对象。然后将该对象移动到返回对象中,我们可以一直对后续表达式进行 prvalue 缩减。所以这基本上可以简化为:
X __tmp(1);
X x3(std::move(__tmp));
我们仍然有一个动作,但只有一个(我们可以避开链式移动)。它是对引用的绑定,需要存在单独的X
对象。参数arg
和make_X
的返回对象必须是不同的对象 - 这意味着必须发生移动。
对于最后两种情况:
X x4 = std::forward<X>(X(1));
X x5 = static_cast<X&&>(X(1));
在这两种情况下,我们都绑定了对 prvalue 的引用,这再次需要临时具体化转换。然后,在这两种情况下,初始值设定项都是一个 xvalue,所以我们没有得到 prvalue 的减少 - 我们只是从 xvalue 移动构造,该 xvalue 是一个具体的临时对象。
因为在表达式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-unqualified 类型的类对象时,可以通过将临时对象直接构造到省略的复制/移动的目标中来省略复制/移动操作
在调用make_X(X(1))
复制省略实际发生,但只有一次:
- 第一个 X(1) 创建一个绑定到
arg
的临时。 - 然后
X(std::forward<T>(arg))
调用移动构造函数。arg
不是临时的,因此上述第二条规则不适用。 - 然后,表达式
X(std::forward<T>(arg))
的结果也应该被移动以构造返回值,但此移动将被省略。
关于您的更新,std::forward
导致绑定到x值的临时X(1)
的实现:std::forward
的返回。此返回的 xvalue 不是临时的,因此复制/省略不再适用。
再次,如果发生移动遗漏,在这种情况下会发生什么。(c++ 语法不是上下文相关的):
auto x7 = std::forward<X>(std::move(x2));
Nota:在我看到关于C++17的新答案后,我想增加困惑。
在 C++17 中,prvalue
的定义发生了变化,即示例代码中不再有任何要 elide 的移动构造函数。以下是 GCC 的结果代码示例,选项在 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
为了简化您的示例:
auto x1 = make_X(1); // converting
auto x2 = X(X(1)); // converting
auto x4 = X(std::forward<X>(X(1))); // converting + move
来自 cppreference 的复制 elision 文档(强调我的):
在 c++17 之前:
在以下情况下,允许使用编译器,但 不需要省略复制和移动(自 C++11 起)构造 类对象...
- 如果函数按值返回类类型,则返回 语句的表达式是非易失性对象的名称,具有 自动存储持续时间,这不是函数参数,也不是 catch 子句参数,并且具有相同的类型(忽略 顶级 CV 资格)作为函数的返回类型,则 复制/移动(自 C++11 起)被省略。当该本地对象是 构造,它直接在存储中构造,其中 否则,函数的返回值将被移动或复制到。这 复制 elision 的变体称为 NRVO,"命名返回值 优化"。
从 c++17 开始:
在以下情况下,编译器需要省略 复制和移动结构...
a) 在初始化中,如果初始值设定项表达式是 prvalue 并且 源类型的 cv 非限定版本与 目标的类,初始值设定项表达式用于 初始化目标对象:
T x = T(T(T())); // only one call to default constructor of T, to initialize x
b) 在函数调用中,如果return 语句的操作数是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
都不符合要求,因为它的结果是一个x值,而不是一个 prvalue:它不会按值返回类类型。因此,不会发生省略。
- 了解构造函数在移动、复制、赋值语义中的行为
- 如何检索指向在单独线程上运行的函数的移动指针?
- 控件不会在选择函数旁边移动
- 移动构造函数与移动赋值
- 将参数传递给构造函数和成员函数时移动或复制
- 为什么对象可以"moved"甚至缺少移动构造函数和移动赋值运算符?
- "<某些系统标头>:错误:"<在此处插入函数>"在移动源文件后不是"std"的成员
- C++ 默认构造函数在移动和复制构造函数存在时不随"using"继承
- 查找所有移动构造函数和移动赋值运算符(特别是那些没有"noexcept"的运算符)
- C++复制构造函数和移动语义之间的区别
- 复制构造函数替换移动构造函数
- 在C 中,为什么在嵌套的unordered_map上调用函数需要移动构造函数
- 移动构造函数和移动赋值运算符与复制省略
- 为什么尽可能不使用"make_x()"函数省略移动构造函数?
- 如何创建可变参数模板函数来移动参数值并处理左值和右值
- 移动构造函数和移动分配. 基类中的运算符
- 为什么复制构造函数与移动构造函数一起调用?
- 编译器不生成移动构造函数并移动分配
- 析构函数是移动ctor/赋值的RHS上唯一调用过的东西吗
- 如何使用继承(抽象基类)实现移动构造函数和移动赋值运算符