在作用域结束时,可以将左值视为右值吗?

6

编辑:考虑以下两个例子:

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即将被销毁。显然,它可以在两种情况下被视为rvalue并进行移动。在(1)中,其内容可以移动到x中,在(2)中可以移动到X().y_的临时实例中。
我的问题是:
1)它是否会在上述任何一个示例中被移动? a) 如果是,根据什么标准规定? b) 如果不是,为什么?这是标准中的遗漏还是我没有考虑到的其他原因?
2)如果上面的答案是否定的。在第一个示例中,我可以将(1)更改为x = std::move(y)以强制编译器执行移动。在第二个示例中,我该怎么做才能告诉编译器y可以移动?return std::move(y)
注:我故意返回Y的实例而不是X,以避免(N)RVO。

我为了简单起见使用了 std::string。但如果它是一个重量级对象或者持有数兆字节数据的非 COW 字符串呢? - Super-intelligent Shade
1
@tadman 复制字符串并非便宜的操作。尤其是当字符串很大,正如楼主所言,这可能会非常昂贵。 - NathanOliver
@NathanOliver 嗯,我认为现代的 std::string 实现使用了一种写时复制跟踪机制,以使得复制更加便宜。在这里进行一些测试后,我发现性能下降了,尽管只有超过 16KB 的字符串才能测量出来。 - tadman
@NathanOliver 这个答案阐明了这一点,所以这真是个惊喜。感谢你指出来! - tadman
2个回答

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):

在以下复制初始化上下文中,可能使用移动操作而不是复制操作:

  • 如果返回语句(9.6.3)中的表达式是一个(可能带括号的)id表达式,该表达式命名了一个具有自动存储期限的对象,在最内层的封闭函数或lambda表达式的主体或参数声明子句中声明,

[...]

选择复制的构造函数的重载决议首先像处理右值那样对对象进行。如果第一个重载解析失败或未执行,或者所选构造函数的第一个参数的类型不是指向对象类型(可能带有cv限定符)的右值引用,则再次执行重载解析,将对象视为左值。

你的返回语句中的表达式(y)是一个id表达式,该表达式命名了一个具有自动存储期限的对象,在最内层的封闭函数的主体中声明,因此编译器必须进行两阶段重载决议。

然而,此时它正在寻找创建XY的构造函数。 X定义了一个(且仅有一个)这样的构造函数,但是该构造函数通过值接收其Y。由于这是唯一可用的构造函数,因此在重载决议中“获胜”。由于它通过值接收其参数,因此我们首先尝试将y作为右值进行重载决议并没有什么区别,因为X没有正确类型的构造函数来接收它。

现在,如果我们像这样定义了X

struct X
{
    X(Y &&y);
    X(Y const &y); 

    Y y_;
}

如果使用两阶段重载解析,则会产生真正的效果 - 即使y指定为左值,第一轮重载解析也将其视为右值,因此将选择X(Y &&y)并用于创建返回的临时对象X - 也就是说,我们将获得移动而不是复制(即使y是左值,并且我们有一个接受左值引用的复制构造函数)。


你熟悉12.8/32吗?它不适用于第二个问题吗? - Super-intelligent Shade
@innocent 你似乎认为这些问题是不同的。你错了。 - Yakk - Adam Nevraumont
n4296 - Super-intelligent Shade
@Yakk 我相信它们是同一个硬币的两面,这就是为什么我在同一个问题中问了它们。 - Super-intelligent Shade
非常好的回答!感谢您花时间理解我的问题 :) - Super-intelligent Shade

-3
为什么不直接用outer来做所有的事情呢?或者如果在函数中使用,可以传递&outer。然后,在函数内部使用*inner

3
谢谢您的建议。以上是一个过于简化的例子。如果您没有问题的答案,请不要回答。 - Super-intelligent Shade

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接