为什么rvalue引用作为返回类型不能初始化非const引用?

4

我阅读了这个问题,我知道rvalue引用是lvalue。

然而,在这段代码示例1中,

int &&fun() {
    return 1;
}

int main() {
    int &a = fun();
}

我来翻译一下,这段内容的意思是:“当我编译它时:”。
 error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'

所以C++编译器告诉我fun的返回类型是一个右值。

一个右值引用如何变成一个右值?

我认为编译器应该以相同的方式处理左值引用和右值引用,但这段代码,例如2,

int & fun(){
    int b;
    return b;
}
int main(){
    int & a=fun();
}

可以进行编译(尽管我收到了一个警告)。

我认为fun的返回类型可能在某个时候发生了变化。

尝试编译示例3:

int &&fun() {
    return 1;
}

int main() {
    decltype(fun()) b = 1;
}

编译成功。因此,我可以说fun的返回类型确实是一个右值引用。

那么,为什么右值引用会变成右值呢?

这里有第四个例子:

int &&a = 1;
int &b = a;

它编译并告诉我们rvalue引用可以绑定到lvalue引用。

现在,对于这两个问题:

  1. 在示例1中,fun()是rvalue吗?
  2. 在示例1中,fun()是rvalue引用吗?

示例3告诉我们fun()是rvalue引用,示例4告诉我们rvalue引用可以绑定到lvalue引用(包括const和非const)。那么为什么示例1中的fun()不能绑定到lvalue引用呢?

示例4还表明rvalue引用是一个lvalue,但是示例1中的编译错误告诉我们,在示例3中被证明是rvalue引用的fun()是一个rvalue。那么,rvalue引用是lvalue还是rvalue?

如果原因是fun()只是一个存在一会儿就会消失的表达式,为什么示例2中的fun()不被认为是rvalue,而它也只是一个没有名称的表达式?一个返回lvalue引用和rvalue引用的函数表达式之间有什么区别?

5个回答

3

我知道一个右值引用是一个左值。

你在谈论两件不同的事情:类型和值类别。例如:

int&& ri = 0; // ri's type is rvalue reference (to int)
              // ri's value category is lvalue; it's a named variable.

根据你的第一个示例,fun() 返回一个 xvalue,它属于 rvalues。
以下表达式是 xvalue 表达式: - 返回类型为右值引用的函数调用或重载运算符,如 std::move(x)
因此,
int &a = fun(); // fails; lvalue-reference can't bind to rvalue

在第二个示例中,fun() 返回的是一个左值表达式,
以下表达式都是左值表达式:
- 具有左值引用返回类型的函数调用或重载运算表达式,例如 std::getline(std::cin, str)std::cout << 1str1 = str2 或者 ++it
int & a=fun(); // fine; lvalue-reference could bind to lvalue

在第三个样例中,
decltype(fun()) b = 1; // the return type of fun() is rvalue-reference;
                       // this has nothing to do with the value category of its return value
                       // b's type is rvalue-reference too, btw its value category is lvalue

在第四个样例中,
int &&a = 1; // fine; rvalue-reference could bind to rvalue
             // a's type is rvalue-reference, its value category is lvalue
int &b = a;  // fine; lvalue-reference could bind to lvalue
             // b's type is lvalue-reference, its value category is lvalue

严格来说,值类别是表达式的属性,而不是变量的属性。 - xskxzr

1

非const引用无法绑定到右值,就是这么简单。

int & a=fun();

不起作用是因为a是一个非const引用,而fun()是一个rvalue表达式。
在第二种情况下,fun()返回一个非const左值引用,它可以绑定到另一个非const引用,当然。

decltype(fun()) b=1;

这段代码之所以能够运行,是因为decltype(fun())的类型是int &&,可以将其绑定到整型字面值1上。


在示例1中,fun()是一个rvalue吗?

是的。

在示例2中,fun()是一个rvalue引用吗?

不是,它是一个lvalue引用。

示例3告诉我们,fun()是一个rvalue引用,示例4告诉我们rvalue引用可以绑定到lvalue引用(包括const和非const)。那么为什么示例1中的fun()不能绑定到lvalue引用?

因为函数fun返回一个rvalue引用,但fun()本身是一个rvalue表达式fun()是一个rvalue。

示例4还表明,rvalue引用是一个lvalue,但示例1中的编译错误告诉我们,在示例3中被证明是rvalue引用的fun()是一个rvalue。那么,rvalue引用是lvalue还是rvalue?

rvalue引用是lvalue。

如果原因是fun()只是一个存在于短暂时间并立即消失的表达式,为什么在示例2中fun()不被视为rvalue,而它也只是一个没有名称的表达式?一个返回lvalue引用和rvalue引用的函数表达式之间有什么区别呢?因为在示例2中,fun()是一个lvalue。来自N4296,§3.10/1.1:[...]返回类型为lvalue引用的函数调用的结果是lvalue。

关于示例2中出现的警告,您应该显示确切的信息。这可能只是因为您返回了对本地变量的引用。局部变量的生命周期有限,因此在其生命周期之外引用它们是未定义的行为,因此会出现警告。


我认为你遇到了一个冲突,即 rvalue 引用是 rvalue(来自示例 1 和示例 3),这与 https://dev59.com/vF4b5IYBdhLWcg3w9Fyr 不一致。顺便说一下,我已经更详细地澄清了我的问题。谢谢 :) - namasikanam
@user482214,我从未提到rvalue引用是rvalue。我将根据您的编辑更新我的答案。 - cadaniluk

1
首先,这段代码表现出未定义的行为:
int && fun(){
    return 1;
}

在这里,你返回了一个悬空引用到1,它超出了作用域。
引用不是指针的另一种语法,而是已经存在的对象的另一个名称。要理解这一点,有必要查看引用初始化规则: 第一个引用初始化规则规定引用可以被初始化(“绑定”)到与之兼容的值上。这意味着
  • int& 可以绑定到 int&
  • int&& 可以绑定到 int&&
  • const int& 可以绑定到 int&
为了理解这一点,最好查看引用初始化规则:
在这种情况下,右侧的实际引用值不会被检索,而是直接绑定到新的引用上。 请注意,int&与int&&不兼容,它们是不同的类型。 第二个引用初始化规则指出,const lvalue引用(const int&)和rvalue引用(int&&)可以绑定到:
  • xvalue或prvalue
  • 作为其他任何事情的最后手段
在后一种情况下,引用绑定到表达式的结果。在const int& x = fun()的情况下,首先会"实例化"(检索)调用fun()的结果,然后将其值绑定到引用上。
但是要想实现这一点,左值引用必须是const。这就是为什么错误提示指出一个非constint&不能绑定到int,因为int是通过评估fun()得出的结果。

1
关键在于表达式的值类别不仅取决于其类型,例如。
int&& a = 1;
int&& fun();
// int&& ri = a; // ill-formed, the expression a is of type int&&, but is an lvalue
int&& ri = fun(); // ok, the expression fun() is of type int&&, and is also an rvalue

此外,正如rustyx在他的答案中指出的那样,函数定义
int && fun(){
    return 1;
} 

在编程中,返回一个临时对象可能会导致未定义的行为,因为该临时对象将在 return 语句执行后立即被销毁。

0

我认为您混淆了rvaluervalue引用。在您的第一个示例中

int && fun(){
    // 1 is an rvalue so it can be bound to an rvalue reference
    // this will produce undefined behavior though because you
    // a returning a dangling reference to an temporary that will
    // go out of scope at the end of this function
    return 1;
}
int main(){
    // you are trying to take a reference to a temporary object.
    // this is (deliberately) not valid
    int & a=fun();

    // One legal way of doing this is by declaring your reference const:
    const int& b = fun(); 
    // because this extends the lifetime of the temporary object returned
    // by fun() to match the lifetime of the reference.
}

在你的第二个例子中:
int & fun(){
    // you have allocated an new int in the free store so the 
    // lifetime of this int is until the main exits. The return
    // type here is an lvalue that can be safely bound to an 
    // lvalue reference
    return *(new int);
}
int main(){
    // binding lvalue reference to lvalue this is ok
    int & a=fun();
}

在你的第三个例子中。
int && fun(){
    // 1 is an rvalue and can be bound to an rvalue reference
    return 1;
}
int main(){
    // decltype(fun()) is equal to int&& so it's ok to bind
    // an rvalue reference to an rvalue
    decltype(fun()) b=1;
}

非常感谢,我修改了Ex 2并添加了Ex 4。首先,在Ex 2中,我认为我可以返回一个int的引用,其生命周期仅限于函数作用域,因此它也是一个临时对象,与Ex 1中的对象相同。其次,我认为Ex 3告诉我们函数的返回类型是一个右值引用,它是一个左值,并且可以绑定到一个左值引用(与Ex 1不同)。 - namasikanam
通过使用new,您可以将值的生命周期延长到函数范围之外,因此如果您不delete该值,则会出现所谓的“内存泄漏”。在示例4中,a是一个左值,因为它有一个名称并且我可以获取其地址,因此将左值引用b绑定到一个左值(int&& a)上是可以的,这个左值恰好是一个右值引用。如果正在被引用的临时对象发生了什么事情,b仍然在当前作用域中持有对a的有效引用。 - StaticBeagle

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