一个函数返回一个rvalue reference有意义吗?

29
这样的签名有哪些有效的用例?
T&& foo();
那么,rvalue引用只用于作为参数吗? 如何使用这样的函数?
T&& t = foo(); // is this a thing? And when would t get destructed?

6
「std::move」是常用的函数,它返回「T&&」类型。此外,「std::optional::value」也有「T&&」类型的重载函数。另外还有一个「const T&&」类型的重载函数,不过我不理解其含义。 - François Andrieux
1
@FrançoisAndrieux 还有 std::get 函数族。 - Brian Bi
1
请查看此答案 - lubgr
这不是一个转发引用吗? - user2357112
3个回答

25

对于一个自由函数来说,返回右值引用并没有太多意义。如果它是一个非静态的局部对象,则永远不应该返回指向它的引用或指针,因为函数返回后它将被销毁。但是,将一个传递给函数的对象作为右值引用返回可能是有意义的,这真的取决于是否合理。

一个可以极大受益于返回右值引用的东西是临时对象的成员函数。假设你有一个

class foo
{
    std::vector<int> bar;
public:
    foo(int n) : bar(n) {}
    std::vector<int>& get_vec() { return bar; }
};

如果你这样做

auto vec = foo(10).get_vec();

你必须复制,因为 get_vec 返回的是一个左值。如果你使用

class foo
{
    std::vector<int> bar;
public:
    foo(int n) : bar(n) {}
    std::vector<int>& get_vec() & { return bar; }
    std::vector<int>&& get_vec() && { return std::move(bar); }
};

那么vec将能够移动由get_vec返回的向量,并且您可以避免进行昂贵的复制操作。


1
@NathanOliver 好的,你可以在任何 rvalue 上调用它(例如 std::move(a).get_vec())。我的意思是,你可能会返回一个即将被销毁的对象的引用。这与返回对局部函数变量的引用相同,存在相同的问题。 - Cruz Jean
5
在这种情况下,std::vector<int> get_vec() && { return std::move(bar); } 999/1000 次都更好。你能想出一个更好的例子吗? - Yakk - Adam Nevraumont
1
@JarekC 那个问题是关于临时变量的。但你没有临时变量,你有一个左值。此外,在这里实际上没有移动操作发生,a仍然存在,所以v仍然是对a内部向量的有效引用。 - NathanOliver
1
@JarekC 这个重载实际上只是用来像 auto bar = foo{42}.get_vec(); 这样使用的。 - NathanOliver
@CruzJean,与下面的答案类似的评论:如果我们“不应该”返回&&,那么例如std::optionaloperator*()中为什么会这样做呢? - Jarek C
显示剩余14条评论

6
T&& t = foo(); // is this a thing? And when would t get destructed?

右值引用与左值引用非常相似。可以将其看作是普通引用的一个例子:

T& foo();

T& t = foo(); // when is t destroyed?

答案是只要该对象仍然存在,t 仍然有效。

对于您的右值引用示例,仍然适用相同的答案。


但是,返回一个右值引用有意义吗?

有时候会有意义,但非常少见。

考虑以下情况:

std::vector<int> v = ...;

// type is std::tuple<std::vector<int>&&>
auto parameters = std::forward_as_tuple(std::move(v));

// fwd is a rvalue reference since std::get returns one.
// fwd is valid as long as v is.
decltype(auto) fwd = std::get<0>(std::move(parameters));

// useful for calling function in generic context without copying
consume(std::get<0>(std::move(parameters)));

所以,确实有一些示例。这里,另一个有趣的例子:
struct wrapper {

    auto operator*() & -> Heavy& {
        return heavy;
    }

    auto operator*() && -> Heavy&& {
        return std::move(heavy);
    }

private:
    Heavy instance;
};

// by value
void use_heavy(Heavy);

// since the wrapper is a temporary, the
// Heavy contained will be a temporary too. 
use_heavy(*make_wrapper());

1
在上一个例子中,如果您用于此的“wrapper”实例是临时的,则operator*()返回对“instance”的引用,该引用同样是临时的。因此,在函数返回后,您将拥有对生命周期已结束(未定义行为)的对象的引用。 - Cruz Jean
你确定不需要 decltype(auto) fwd = std::get<0>(std::move(parameters)); 吗? - Yakk - Adam Nevraumont
@CruzJean,我同意你的说法,但另一方面,我真的不明白为什么例如T&& std::optional<T>::operator*() &&被定义成完全按照 @GuillaumeRacicot 所提出的那样运作。我本来期望的是T std::optional<T>::operator*() &&,但实际上并不是这样... - Jarek C

1
我认为一个使用案例是显式地授权“清空”一些非本地变量。可能像这样:

我认为一个使用案例是显式地授权“清空”一些非本地变量。可能像这样:

class Logger
{
public:
    void log(const char* msg){
        logs.append(msg);
    }
    std::vector<std::string>&& dumpLogs(){
        return std::move(logs);
    }
private:
    std::vector<std::string> logs;
};

但我承认现在是编造的,我从未真正使用过它,也可以像这样完成:

std::vector<std::string> dumpLogs(){
    auto dumped_logs = logs;
    return dumped_logs;
}

1
这也是其他答案得出的结论,但在调用临时对象的情况下,当函数返回时,您将获得对生命周期已结束(未定义行为)的对象的(rvalue)引用。 - Cruz Jean
@CruzJean 好的,我的回答先在这里,也没有人投票/抱怨,所以我会保留它。我没有返回任何对临时变量的引用。 - Quimby
return std::move(logs); 这里返回值是一个引用类型,而 *this 是一个临时对象(因为它是一个右值方法),这使得 this->logs 也成为了一个临时对象。这就是我所说的对临时对象的引用。 - Cruz Jean
@CruzJean 你是指例如 auto&& x = Logger{}.dumpLogs(); 吗?是的,那是悬空引用,但无论它们返回什么类型的引用,所有getter都会发生这种情况。我的用例将类似于 Logger l; /*调用log()*/, auto logs = l.dumpLogs();/*再次记录..并重复。*/ - Quimby

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