范围末尾的左值可以被视为右值吗?
Can an lvalue at end of scope be treated as an rvalue?
编辑: 考虑以下 2 个示例:
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。
为什么不对所有内容都使用 outer
?或者,如果使用函数传递 &outer
。然后,在函数内部使用 *inner
.
第一个例子
对于你的第一个例子,答案显然是“不”。该标准允许编译器在处理函数中的 return 值时对副本采取各种自由(即使有副作用)。我想,在 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”的代码。确实没有具体的引文——相反,有引文谈论函数中 return 值的异常,这在此处不适用,因为我们不处理函数中的 return 值.
至于为什么没有人处理这个:我猜这只是因为没有人打扰。函数的返回值经常发生,因此值得花大量精力对其进行优化。创建一个非功能块,并在块中创建一个值,您继续将其复制到块外的值以保持其可见性,这种情况很少发生,以至于似乎不太可能有人写出提案。
第二个例子
这个例子是至少return从一个函数中获取一个值——所以我们必须查看允许移动而不是复制的异常的细节.
这里的规则是(N4659, §[class.copy.elision]/3):
In the following copy-initialization contexts, a move operation might be used instead of a copy operation:
- If the expression in a return statement (9.6.3) is a (possibly parenthesized) id-expression that names an object with automatic storage duration declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression,
[...]
overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. If the first overload resolution fails or was not performed, or if the type of the first parameter of the selected constructor is not an rvalue reference to the object’s type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue.
您的 return 语句中的表达式 (y
) 是一个 id 表达式,它命名一个对象,该对象具有在最内层封闭函数的主体中声明的自动存储持续时间,因此编译器必须做两阶段过载决议。
然而,此时它正在寻找的是一个从 Y
创建 X
的构造函数。 X
定义了一个(并且只有一个)这样的构造函数——但该构造函数通过值 接收其 Y
。因为这是唯一可用的构造函数,所以它是在重载决策中“获胜”的构造函数。由于它按值获取参数,我们首先尝试将 y
视为右值的重载决策这一事实并没有真正产生任何影响,因为 X
没有正确类型的构造函数收到。
现在,如果我们定义了X
这样的东西:
struct X
{
X(Y &&y);
X(Y const &y);
Y y_;
}
...then 两阶段重载决议会产生真正的效果——即使 y
指定一个左值,第一轮重载决议对待它作为一个右值,所以 X(Y &&y)
将被选择并用于创建临时 X
得到 returned——也就是说,我们得到一个移动而不是一个副本(即使虽然 y
是一个左值,但我们有一个采用左值引用的复制构造函数)。
编辑: 考虑以下 2 个示例:
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。
为什么不对所有内容都使用 outer
?或者,如果使用函数传递 &outer
。然后,在函数内部使用 *inner
.
第一个例子
对于你的第一个例子,答案显然是“不”。该标准允许编译器在处理函数中的 return 值时对副本采取各种自由(即使有副作用)。我想,在 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”的代码。确实没有具体的引文——相反,有引文谈论函数中 return 值的异常,这在此处不适用,因为我们不处理函数中的 return 值.
至于为什么没有人处理这个:我猜这只是因为没有人打扰。函数的返回值经常发生,因此值得花大量精力对其进行优化。创建一个非功能块,并在块中创建一个值,您继续将其复制到块外的值以保持其可见性,这种情况很少发生,以至于似乎不太可能有人写出提案。
第二个例子
这个例子是至少return从一个函数中获取一个值——所以我们必须查看允许移动而不是复制的异常的细节.
这里的规则是(N4659, §[class.copy.elision]/3):
In the following copy-initialization contexts, a move operation might be used instead of a copy operation:
- If the expression in a return statement (9.6.3) is a (possibly parenthesized) id-expression that names an object with automatic storage duration declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression,
[...]
overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. If the first overload resolution fails or was not performed, or if the type of the first parameter of the selected constructor is not an rvalue reference to the object’s type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue.
您的 return 语句中的表达式 (y
) 是一个 id 表达式,它命名一个对象,该对象具有在最内层封闭函数的主体中声明的自动存储持续时间,因此编译器必须做两阶段过载决议。
然而,此时它正在寻找的是一个从 Y
创建 X
的构造函数。 X
定义了一个(并且只有一个)这样的构造函数——但该构造函数通过值 接收其 Y
。因为这是唯一可用的构造函数,所以它是在重载决策中“获胜”的构造函数。由于它按值获取参数,我们首先尝试将 y
视为右值的重载决策这一事实并没有真正产生任何影响,因为 X
没有正确类型的构造函数收到。
现在,如果我们定义了X
这样的东西:
struct X
{
X(Y &&y);
X(Y const &y);
Y y_;
}
...then 两阶段重载决议会产生真正的效果——即使 y
指定一个左值,第一轮重载决议对待它作为一个右值,所以 X(Y &&y)
将被选择并用于创建临时 X
得到 returned——也就是说,我们得到一个移动而不是一个副本(即使虽然 y
是一个左值,但我们有一个采用左值引用的复制构造函数)。