编辑:请考虑以下两个示例:
std::string x;
{
std::string y = "extremely long text ...";
...
x = y; // *** (1)
}
do_something_with(x);
struct Y
{
Y();
Y(const Y&);
Y(Y&&);
... // many "heavy" members
};
struct X
{
X(Y y) : y_(std::move(y)) { }
Y y_;
}
X foo()
{
Y y;
...
return y; // *** (2)
}
在两个例子中,第(1)和(2)行中的y
已接近其生命周期并即将被销毁。很明显,它可以被视为右值并在两种情况下都可以移动。在(1)中,其内容可以移入x
和(2)中X().y_
的临时实例。
我的问题是:
1)是否会在上述任何一个例子中移动? (a)如果是,则采用何种标准规定。 (b)如果不是,为什么不呢?这是标准中的遗漏还是我没有想到的其他原因?
2)如果上述答案为否。在第一个例子中,我可以将(1)更改为x = std::move(y)
以强制编译器执行移动。在第二个示例中,我可以做什么来向编译器指示可以移动y
? return std::move(y)
?
注意:我故意在(2)中返回Y
而不是X
的实例以避免(N)RVO。
答案 0 :(得分:8)
对于你的第一个例子,答案显然是“不”。该标准允许编译器在处理函数的返回值时对副本(即使有副作用)采取各种自由。我想,在std::string
的特定情况下,编译器可以“知道”复制和移动都没有任何副作用,因此它可以在as-if规则下用一个代替另一个。但是,如果我们有类似的东西:
struct foo {
foo(foo const &f) { std::cout << "copy ctor\n"; }
foo(foo &&f) { std::cout << "move ctor\n"; }
};
foo outer;
{
foo inner;
// ...
outer = inner;
}
......正常运行的编译器必须生成打印出“copy ctor”的代码,而不是“move ctor”。实际上没有具体的引用 - 相反,有引用讨论函数返回值的异常,这里不适用,因为我们没有处理函数的返回值。
至于为什么没有人处理这个问题:我猜这只是因为没人感到困扰。从函数中返回值经常发生,因此值得花费大量精力来优化它。创建一个非功能块,并在块中创建一个值,该块将继续复制到块外部的值以保持其可见性,这种情况很少发生,以至于任何人都不可能写出提案。
此示例 至少从函数返回一个值 - 因此我们必须查看允许移动而不是复制的异常的细节。
这里的规则是(N4659,§[class.copy.elision] / 3):
在以下复制初始化上下文中,可能会使用移动操作而不是复制操作:
- 如果return语句(9.6.3)中的表达式是一个(可能带括号的)id-expression,它指定一个对象,该对象具有在最内层封闭函数或lambda-的body或parameter-declaration-clause中声明的自动存储持续时间。表达,
[...]
首先执行重载决策以选择复制的构造函数,就好像该对象是由rvalue指定的一样。如果第一个重载决策失败或未执行,或者所选构造函数的第一个参数的类型不是对象类型的rvalue引用(可能是cv-qualified),则再次执行重载决策,将对象视为左值。
return语句中的表达式(y
)是一个id-expression,它指定在最内层封闭函数体中声明的具有自动存储持续时间的对象,因此编译器必须执行两阶段重载解析
但是,它目前正在寻找的是一个从X
创建Y
的构造函数。 X
定义了一个(也是唯一一个)此类构造函数 - 但该构造函数按值接收其Y
。因为那是唯一可用的构造函数,那就是在重载决策中“获胜”的构造函数。由于它按值进行参数化,因此我们首先尝试将y
视为右值的重载决策实际上没有任何区别,因为X
没有正确类型的ctor收到它。
现在, if 我们定义了X
这样的东西:
struct X
{
X(Y &&y);
X(Y const &y);
Y y_;
}
... 然后两阶段重载决策会产生实际效果 - 即使y
指定左值,第一轮重载决策将其视为右值,因此,X(Y &&y)
将被选中并用于创建返回的临时X
- 也就是说,我们将获得移动而不是副本(即使y
是左值,我们有一个带左值引用的复制构造函数。
答案 1 :(得分:-3)
为什么不使用render
作为一切?或者如果使用函数传递outer
。然后,在函数内部使用&outer
。