【问题标题】:Is this safe? Rvalue and reference return values这安全吗?右值和参考返回值
【发布时间】:2017-06-19 23:29:40
【问题描述】:
#include <iostream>
#include <string>

using namespace std;

class Wrapper
{
public:
    std::string&& get() &&
    {
        std::cout << "rvalue" << std::endl;
        return std::move(str);
    }

    const std::string& get() const &
    {
        std::cout << "lvalue" << std::endl;
        return str;
    }
private:
    std::string str;
};

std::string foo()
{
    Wrapper wrap;
    return wrap.get();
}

std::string bar()
{
    Wrapper wrap;
    return std::move(wrap.get());
}

std::string fooRvalue()
{
    return Wrapper().get();
}

std::string barRvalue()
{
    return std::move(Wrapper().get());
}

int main() {
    while(1)
    {
        std::string s = foo();
        std::string s2 = bar();
        std::string s3 = fooRvalue();
        std::string s4 = barRvalue();
    }
    return 0;
}

const std::string& 和 std::string&& 的返回值在这个用例中是否安全(假设 std::string 是其他一些不可复制的类型)。我认为它不起作用,因为引用应该指向一些超出范围的局部变量,但它似乎工作得很好?我也看过之前使用的 && 语法,我是否正确使用它(我将它限制在 Wrapper 本身是一个右值时)。

当返回引用是安全的时,我只是有点困惑,我认为规则是对象必须比返回的值长,但我以前见过标准库和提升返回引用之类的东西。如果我将他们给我的引用存储在一个变量中(这不是一个引用),那么如果我返回那个存储的变量,一切似乎都可以正常工作。谁能帮我解决一下,给我一些好的规则来遵循。

这是我一直在玩的 ideone,似乎一切正常:http://ideone.com/GyWQF6

【问题讨论】:

  • 如果我没记错的话,for (char c : Wrapper{}.get()) { ... } 是不安全的。

标签: c++ c++11 reference c++14 rvalue


【解决方案1】:

是的,这是实现成员 getter 的正确且安全的方式。但是你可以做一个改进,我会讲到的。关键是std::string 成员的生命周期(几乎)与包含它的Wrapper 对象的生命周期相同。并且一个临时的只会在它出现的完整表达式的末尾被销毁,而不是在它被“使用”之后立即被销毁。所以在return std::move(Wrapper().get());中,Wrapper被创建,get()被调用,std::move被调用,返回值被构造,只有Wrapper和它的string被销毁。

唯一的危险是如果有人将另一个引用绑定到您的成员。但这将是他们的错误,普通的 const-reference getter 也有同样的危险。这相当于不正确的const char* ptr = "oops"s.c_str();

std::string foo()
{
    Wrapper wrap;
    return wrap.get();
}

foo 复制从成员构造返回值。希望这不是一个惊喜。

std::string bar()
{
    Wrapper wrap;
    return std::move(wrap.get());
}

bar 也从成员中复制构造返回值。这里发生的情况是,由于wrap 是一个左值,所以调用左值get(),它返回一个const 左值。 (非正式地,“const std::string&amp;”。)然后move 将其转换为一个 xvalue,但它仍然是const。 (非正式地,“const std::string&amp;&amp;”。)移动构造函数string(string&amp;&amp;) 在这里不能匹配,因为参数是const。但是复制构造函数string(const string&amp;)可以匹配,并被调用。

如果您希望 bar 移动构造返回值,您可能需要为非常量左值添加第三个重载:

std::string& get() & { return str; }

(如果您不想允许修改成员,请注意您已经这样做了。例如,有人可以这样做std::move(wrap).get() = "something";。)

另外,如果你改用return std::move(wrap).get();,即使没有第三次重载,也会移动构造返回值。

std::string fooRvalue()
{
    return Wrapper().get();
}

fooRvalue move 构造返回值,因为使用了右值get()。如前所述,Wrapper 的使用寿命足以保证这种结构的安全。

std::string barRvalue()
{
    return std::move(Wrapper().get());
}

barRvalue 中,move 调用绝对没用。表达式Wrapper().get() 已经是一个xvalue,所以move 只是将std::string 类型的xvalue 转换为...std::string 类型的xvalue。不过还是一样安全。

【讨论】:

  • 你的回答太棒了。很少有问题,右值或左值何时会变成 xvalue。还有两个你提到的,“如果你希望 bar 移动构造返回值,你可能想要为非常量左值添加第三个重载”。现在我不完全理解为什么允许有人修改我返回的 const 引用(它应该是 const,对吗?),但现在知道他们可以返回一个非常量引用而不是返回一个更好的const 引用(因为我确实希望 bar 移动构造)
  • std::move 用于将任何表达式转换为 xvalue。 (您也可以强制转换为右值引用类型,但move 更方便。)不允许任何人修改您返回的 const 引用,如果使用了 const 左值重载。 std::move(wrap).get() = "something"; 中的技巧是使用另一个重载。请注意,如果您以 const Wrapper 对象开头,则无法修改该成员。
  • 最后一个问题,感谢。假设 wrapper 包含 unique_ptr(不可复制),并且我删除了 const 限定符,因此 get 现在看起来像 std::unique_ptr&amp; get()。如果此包装器的用户编写这样的代码(以便连续调用两次 get),auto contents = wrap.get() auto contents2 = wrap.get() 内容变量将被移动到正确的位置?而且由于左值 get() 被调用两次,一次移动会发生两次并导致坏事?
  • auto contents = wrap.get(); 不会编译,因为返回的左值不能绑定到移动构造函数的右值引用,并且复制构造函数被删除。
【解决方案2】:

函数的返回值对象将在return 表达式中创建的局部变量和临时变量的销毁之前初始化。因此,从局部变量/临时变量转换为函数返回值是完全合法的。

这四个全局函数之所以有效,是因为它们返回的是对象,而不是对象的引用。并且该对象在其构造函数参数引用被破坏的任何本地对象之前被构造。

barRvalue 相当多余(因为Wrapper().get() 的结果已经是一个右值引用),但可以正常工作。

当返回引用是安全的时,我只是有点困惑

返回对已销毁对象的引用的函数是不安全的。返回对本地对象或本地对象的一部分的引用是不安全的。

返回对*this*this 某些部分的引用是安全的,因为this 对象的寿命将超过返回引用的函数。这与返回对您通过指针/引用获取的参数的引用没有什么不同。该对象将比返回引用的函数调用寿命更长。

【讨论】:

  • “返回对局部变量或局部变量一部分的引用是不安全的”。包装器的 .get() 方法不是局部变量的一部分吗?包装器应该只在该函数存在时才存在,.get() 的返回值是否被复制?
  • @StephenEckels: this 不是局部变量;它是传递给每个非静态成员函数的隐式参数。它指的是被调用的对象,除非成员函数直接销毁该对象,否则该对象在引用返回后保证存在。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-10-05
  • 2021-11-14
  • 1970-01-01
  • 1970-01-01
  • 2010-10-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多