从右值引用限定的方法中移动还是不移动?

15

在下面的 C++11+ 代码中,应该优先选择哪种 return 语句构造方式?

#include <utility>


struct Bar
{
};

struct Foo
{
    Bar bar;

    Bar get() &&
    {
        return std::move(bar); // 1
        return bar;            // 2
    }
};

1
我不认为这个方法需要移动值,我建议将其作为独立的方法并赋予适当的名称,因为“get”方法通常不仅能够工作一次(方法的r-value限定符在这方面起不到作用)。 - user7860670
4
确实如此,它扮演了一个重要的角色。如果您获取一个函数的返回值,那么您肯定希望仅对该临时对象调用一次。为什么要建议故意降低性能呢? - StoryTeller - Unslander Monica
1
@VTT的原因是为了防止复制。这里方法的名称并不重要,这只是一个简单的示例代码。 - Constructor
3个回答

20

既然这是一个r-value引用的成员函数,this很可能即将过期。因此,假设Bar从移动中获益,移动bar是有意义的。

由于bar是一个成员变量,而不是本地对象/函数参数,在返回语句中进行副本省略的常规标准并不适用。除非你明确使用std::move,否则它总是会复制。

所以我的答案是选择选项一。


4
根据使用情况,您可能需要为 &const & 添加重载。 - sbabbi
1
我会认为编译器会在选项2中省略复制,并且更喜欢这样做,因为选项1可能通过强制移动来防止省略。或者我完全错了吗? - Jesper Juhl
这里是否可以根据标准应用一些省略呢?这就是问题所在。我的意思是r-value ref-qualified方法的特殊情况。 - Constructor
@JesperJuhl - 你正在思考这个部分。它不适用于数据成员。但根据as-if规则,事情可能会发生,我想。然而,我不确定它有多常见。这里是GCC 8,在-O2下并不太聪明。 - StoryTeller - Unslander Monica
@BenVoigt - 怎么会呢?如果正在进行的复制没有可观察到的行为,那么有什么阻止实现将其省略掉呢? - StoryTeller - Unslander Monica
显示剩余8条评论

4
我更喜欢第三个选项:
Bar&& get() &&
// ^^
{
    return std::move(bar);
}

另外,顺便一提:

Bar& get() & { return bar; }
Bar const& get() const& { return bar; }
Bar const&& get() const&& { return std::move(bar); }

我们是一个右值,所以可以自由地利用我们的资源,因此将 bar 进行 move 是正确的。但仅仅因为我们可以移动 bar 并不意味着我们必须强制这样做并产生额外的操作,因此我们应该返回一个右值引用。
这就是标准库的实现方法 - 例如 std::optional<T>::value

这在对象移动不便宜(或由于副作用、异常等原因不希望移动)时特别有用。虽然这些情况很少发生,但确实存在。总的来说,“不要为不需要的东西付费”。 - Sopel
这对于像 for (auto e : Foo{}.get()) { /* ... */ } 这样的事情来说太危险了,不是吗? - Constructor
@Constructor 是的,和任何返回引用的函数一样,有可能出现悬空引用的代码。 - Barry
@Barry 但如果对象是prvalue,则风险更高。值得注意的是,如果您使用此rvalue-ref返回类型,则唯一可使用get()函数并且其有益处的情况是.get().somethingImmediatelyHere(),因为这里会避免移动。对于其他情况,auto f = .get(); 您的函数也将有一个移动操作。 - Johannes Schaub - litb

1
我想澄清一下我的观点(来自评论)。尽管移动结果通常比复制更有效,但这不是我的主要关注点。核心问题源于错误的假设,即通过在 r-value 引用到 Foo 实例上调用此方法,调用者的意图包括创建一个新的 Bar 值。例如:
Foo Produce_Foo(void);

// Alright, caller wanted to make a new `Bar` value, and by using `move`
// we've avoided a heavy copy operation.
auto bar{Produce_Foo().get()};

// Oops! No one asked us to make a useless temporary...
cout << Produce_Foo().get().value() << endl;

解决方案是添加专用函数,仅用于查看已存储的 bar 并控制已存储的 bar 对象的内容。
Bar const & get_bar() const noexcept
{
    return bar;
}

// no particular need to this method to be r-value reference qualified
// because there is no direct correlation between Foo instance being moved / temp
// and intention to take control over content of stored bar object.
Bar give_bar() noexcept
{
    return ::std::move(bar);
}

现在用户有选择了,就不会再出现问题:

// Alright, caller wanted to make a new `Bar` value, and by using `move`
// we've avoided a heavy copy operation.
// There is also no need to figure out whether Produce_Foo returned an rvalue or not.
auto bar{Produce_Foo().give_bar()};

// Alright, no extra temporaries.
cout << Produce_Foo().get_bar().value() << endl;

关于r-value引用限定方法的用例,我认为它们在处理与该对象相同类型的临时变量时最为有用。例如,实现这种连接运算符的字符串类可以减少重新分配的数量,从而像专门的字符串构建器一样执行。

4
你对 Produce_Foo().get().value() 的问题仅仅在于当不必要时通过移动构造函数创建了一个临时对象吗?我没有看到这样做会有任何不安全的地方。而且由于大多数移动构造函数都非常快,所以为了进行小规模的过早优化来使代码变得复杂似乎没有必要。 - aschepler
@aschepler 你所说的不安全是什么意思?所有的C++代码都是不安全的(与安全托管代码相反)。而且,无论选择哪种方法实现,这些代码片段都应该正确地运行。移动构造函数通常比复制构造函数快得多,但在调用临时对象的删除时,你必须小心,以免出现过早优化或“丑化代码”的情况。本答案的重点不是证明使用专用方法可以澄清一切,也不是r值引用限定符在方法上的最佳用例。 - user7860670
1
“不安全”只是指未定义的行为或意外行为。(对其余内容不发表评论。) - aschepler

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