通过右值引用返回是否更有效?

发布于 2024-07-26 23:21:54 字数 108 浏览 6 评论 0 原文

例如:

Beta_ab&& Beta::toAB() const {
    return move(Beta_ab(1, 1));
}

for example:

Beta_ab&& Beta::toAB() const {
    return move(Beta_ab(1, 1));
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

爱人如己 2024-08-02 23:21:54
Beta_ab&&
Beta::toAB() const {
    return move(Beta_ab(1, 1));
}

这会返回一个悬空引用,就像左值引用的情况一样。 函数返回后,临时对象将被破坏。 您应该按值返回 Beta_ab ,如下所示

Beta_ab
Beta::toAB() const {
    return Beta_ab(1, 1);
}

现在,它正确地将临时 Beta_ab 对象移动到函数的返回值中。 如果编译器可以,它将通过使用 RVO(返回值优化)来完全避免移动。 现在,您可以执行以下操作

Beta_ab ab = others.toAB();

,它将把临时构造移动到ab中,或者执行RVO以完全省略移动或复制。 我建议您阅读 BoostCon09 Rvalue References 101 它解释了这个问题,以及如何( N)RVO 恰好与此相互作用。


在其他情况下,返回右值引用的情况是一个好主意。 假设您有一个经常在临时对象上调用的 getAB() 函数。 让它返回右值临时值的 const 左值引用并不是最佳选择。 您可以像这样实现它。请

struct Beta {
  Beta_ab ab;
  Beta_ab const& getAB() const& { return ab; }
  Beta_ab && getAB() && { return move(ab); }
};

注意,在这种情况下,move 不是可选的,因为 ab 既不是本地自动右值,也不是临时右值。 现在,ref-qualifier && 表示在右值临时变量上调用第二个函数,进行以下移动,而不是复制

Beta_ab ab = Beta().getAB();
Beta_ab&&
Beta::toAB() const {
    return move(Beta_ab(1, 1));
}

This returns a dangling reference, just like with the lvalue reference case. After the function returns, the temporary object will get destructed. You should return Beta_ab by value, like the following

Beta_ab
Beta::toAB() const {
    return Beta_ab(1, 1);
}

Now, it's properly moving a temporary Beta_ab object into the return value of the function. If the compiler can, it will avoid the move altogether, by using RVO (return value optimization). Now, you can do the following

Beta_ab ab = others.toAB();

And it will move construct the temporary into ab, or do RVO to omit doing a move or copy altogether. I recommend you to read BoostCon09 Rvalue References 101 which explains the matter, and how (N)RVO happens to interact with this.


Your case of returning an rvalue reference would be a good idea in other occasions. Imagine you have a getAB() function which you often invoke on a temporary. It's not optimal to make it return a const lvalue reference for rvalue temporaries. You may implement it like this

struct Beta {
  Beta_ab ab;
  Beta_ab const& getAB() const& { return ab; }
  Beta_ab && getAB() && { return move(ab); }
};

Note that move in this case is not optional, because ab is neither a local automatic nor a temporary rvalue. Now, the ref-qualifier && says that the second function is invoked on rvalue temporaries, making the following move, instead of copy

Beta_ab ab = Beta().getAB();
本宫微胖 2024-08-02 23:21:54

例如,在稍微不同的上下文中,它可以更加高效:

template <typename T>
T&& min_(T&& a, T &&b) {
    return std::move(a < b? a: b);
}

int main() {
   const std::string s = min_(std::string("A"), std::string("B"));
   fprintf(stderr, "min: %s\n", s.c_str());
   return 0;
}

作为一个有趣的观察,在我的机器上 clang++ -O3 为上面的代码生成 54 条指令,而为常规 std::min。 但是,使用 -O0 时,它会为上述代码生成 518 条指令,而常规 std::min 则为 481 条指令。

更新

对于不同意或不理解这个答案的人,我建议在编译器资源管理器又名 godbolt 中使用这个示例(https://godbolt.org) 在说有问题之前首先:

// use compiler flags: -O3 --std=c++20 -fno-exceptions
#include <utility>
#include <string>

// Example of the `min(a, b)` implementation that can take advantage of return by rvalue.
template <typename T>
T&& min_(T&& a, T &&b) {
    return std::move(a < b? a: b);
}

// Test class that supports both copy and move construction.
struct Bar {
    Bar();
    Bar(const Bar &other);
    Bar(Bar &&other);
    ~Bar();
    char *ptr;
    friend bool operator <(const Bar& lhs, const Bar& rhs);
};

// Just to tell the optimizer that we care about the value.
void consume(const void *);

#if 1
void with_rvalue() {
   const Bar s = min_(Bar(), Bar());
   consume(&s);
}
#else
void without_rvalue() {
   const Bar s = std::min(Bar(), Bar());
   consume(&s);
}
#endif

with_rvalue() 中,我们得到:

        Bar::Bar() [object constructor]
        Bar::Bar() [object constructor]
        operator<(Bar const&, Bar const&)
        Bar::Bar(Bar&&) [move object constructor]
        Bar::~Bar() [object destructor]
        Bar::~Bar() [object destructor]
        consume(void const*)
        Bar::~Bar() [object destructor]

without_rvalue() 中我们得到:

        Bar::Bar() [object constructor]
        Bar::Bar() [object constructor]
        operator<(Bar const&, Bar const&)
        Bar::Bar(Bar const&) [copy object constructor]
        Bar::~Bar() [object destructor]
        Bar::~Bar() [object destructor]
        consume(void const*)
        Bar::~Bar() [object destructor]

正如您所看到的,当按右值返回时,我们避免了对象复制。 该原语的实用性是另一个主题,但这里有一个示例,说明按右值返回如何生成更优化的代码。

It can be more efficient, for example, in a bit different context:

template <typename T>
T&& min_(T&& a, T &&b) {
    return std::move(a < b? a: b);
}

int main() {
   const std::string s = min_(std::string("A"), std::string("B"));
   fprintf(stderr, "min: %s\n", s.c_str());
   return 0;
}

As an interesting observation, on my machine clang++ -O3 generates 54 instructions for code above versus 62 instructions for regular std::min. However, with -O0 it generates 518 instructions for code above versus 481 for regular std::min.

UPDATE

For folks that disagree with or don't understand this answer, I would suggest to play with this sample in Compiler Explorer aka godbolt (https://godbolt.org) first before saying that something is wrong:

// use compiler flags: -O3 --std=c++20 -fno-exceptions
#include <utility>
#include <string>

// Example of the `min(a, b)` implementation that can take advantage of return by rvalue.
template <typename T>
T&& min_(T&& a, T &&b) {
    return std::move(a < b? a: b);
}

// Test class that supports both copy and move construction.
struct Bar {
    Bar();
    Bar(const Bar &other);
    Bar(Bar &&other);
    ~Bar();
    char *ptr;
    friend bool operator <(const Bar& lhs, const Bar& rhs);
};

// Just to tell the optimizer that we care about the value.
void consume(const void *);

#if 1
void with_rvalue() {
   const Bar s = min_(Bar(), Bar());
   consume(&s);
}
#else
void without_rvalue() {
   const Bar s = std::min(Bar(), Bar());
   consume(&s);
}
#endif

In with_rvalue() we get:

        Bar::Bar() [object constructor]
        Bar::Bar() [object constructor]
        operator<(Bar const&, Bar const&)
        Bar::Bar(Bar&&) [move object constructor]
        Bar::~Bar() [object destructor]
        Bar::~Bar() [object destructor]
        consume(void const*)
        Bar::~Bar() [object destructor]

In without_rvalue() we get:

        Bar::Bar() [object constructor]
        Bar::Bar() [object constructor]
        operator<(Bar const&, Bar const&)
        Bar::Bar(Bar const&) [copy object constructor]
        Bar::~Bar() [object destructor]
        Bar::~Bar() [object destructor]
        consume(void const*)
        Bar::~Bar() [object destructor]

So as you can see, when returning by rvalue we avoid an object copy. Practicality of this primitive is another topic, but here you have an example of how returning by rvalue generates more optimal code.

孤芳又自赏 2024-08-02 23:21:54

正如已接受的答案中所指出和解释的那样,示例中的代码返回悬空引用,因为创建了临时引用。 C++26 草案在与 return 语句相关的部分中有以下段落(8.7.4.6):

在返回类型为引用的函数中,除了 std::is_convertible 发明的函数之外,将返回的引用绑定到临时表达式的 return 语句格式不正确。

因此,从 C++26 开始,示例中的代码似乎不再有效。

As noted and explained in the accepted answer, the code in the example returns dangling reference because a temporary is created. C++26 draft has the following paragraph in the section related to return statement (8.7.4.6):

In a function whose return type is a reference, other than an invented function for std​::​is_convertible, a return statement that binds the returned reference to a temporary expression is ill-formed.

So it seems the code in the example will not be valid anymore starting with C++26.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文