一个 rvalue 结构的成员是 rvalue 还是 lvalue?

31

调用返回结构体的函数是rvalue表达式,但它的成员呢?
这段代码在我的g++编译器上运行良好,但gcc报错显示"需要左值作为赋值运算符的左操作数":

struct A
{
    int v;
};

struct A fun()
{
    struct A tmp;
    return tmp;
}

int main()
{
    fun().v = 1;
}

gcc将fun().v视为rvalue,我能理解这一点。但是g++没有认为这个赋值表达式是错误的。这是否意味着在C ++中fun1().v是lvalue?现在的问题是,我搜查了C++98/03标准,没有找到任何关于fun().v是lvalue还是rvalue的说明。那么,它是什么呢?


1
我希望你不介意,但我使它变得容易复制粘贴尝试。 - GManNickG
@*: 在我看来,这个问题应该被命名为“临时rvalue本身是否是成员?”有评论吗?顺便说一句,这是一个好问题。 - dirkgently
@GMan:我记得在某个地方提到过C语言,但是现在我找不到了。现在我只看到g++、c++和C++98/03 :S。 - Sebastian Mach
@dribeas - David Rodríguez:我不确定,我从未听说过struct的存在会改变名称查找;结构体/类/联合体的“名称”是可选的,如果省略,则这些匿名对象的成员将参与到封闭作用域的名称查找中,但“依赖于struct关键字的名称查找”对我来说是新的,我在标准中也找不到相关内容。 - Sebastian Mach
典型的例子是如果你同时拥有一个被命名为Foo的函数和一个结构体。在这种情况下,Foo指的是该函数,而struct Foo则是指该类型。 - MSalters
显示剩余6条评论
6个回答

19

一个rvalue表达式的成员也是一个rvalue。

标准在5.3.5 [expr.ref]中说明:

如果E2被声明为类型“引用T”,那么E1.E2是一个左值[...] - 如果E2是非静态数据成员,且E1的类型为“cq1 vq1 X”,E2的类型为“cq2 vq2 T”,则该表达式指定了第一个表达式所指定的对象的命名成员。如果E1是一个左值,则E1.E2是一个左值。


+1 这是唯一准确的答案。在原始示例中,表达式 E1 是 fun(),E2 是 v,没有 cv 限定符。v 也不是静态的,也不是引用。 - MSalters
如果不是因为你坚持要在第一个答案上更加精确,我就找不到它了(我现在正在删除它)。 - David Rodríguez - dribeas
4
这是第5.2.5节。准确地说,如果E1不是左值,该段落没有指定任何内容。然而,C++0x添加了“否则它是右值”。 - Potatoswatter
2
根据Potatoswatter的指出,在DR421中已经对其进行了修正,增加了一个额外的“否则,它是一个rvalue”。 根据标准委员会的说法,尽管措辞不幸,但意图已经表达清楚:http://anubis.dkuug.dk/jtc1/sc22/wg21/docs/cwg_defects.html#421 - David Rodríguez - dribeas
1
当前的C++17草案更加明确:如果E1是一个左值,那么E1.E2也是一个左值;否则E1.E2是一个xvalue - Arnaud
显示剩余2条评论

3
现在是了解xvaluesglvalues的好时机。 Rvalues有两种类型 - prvaluesxvalues。根据新的C++17标准,

prvalue是一个表达式,其评估初始化对象、位字段或操作数,如出现的上下文所指定。

因此,在您的示例中类似于fun()的内容会评估为prvalue(即rvalue)。这也告诉我们fun().v不是prvalue,因为它不是普通初始化。 Xvalues也是rvalue,定义如下

xvalue(“eXpiring”值)也引用一个对象,通常接近其生命周期的结束(以便可以移动其资源,例如)。涉及rvalue引用(8.3.2)的某些类型的表达式会产生xvalue。[例如:调用返回对象类型的rvalue引用的函数的结果是xvalue(5.2.2)。-end example]

除了rvalues之外,另一个大类别是glvalue,可以是两种类型的xvalues和传统的lvalues
到目前为止,我们已经定义了基本的值类别。可以将其可视化如下:

enter image description here

glvalue类别可以广泛地被认为是在移动语义成为一种事物之前,lvalues所应该表示的东西 - 可以在表达式的左侧的事物。 glvalue表示广义lvalue。
如果我们看一下xvalue的定义,那么它说如果某个东西接近生命周期的结束,就是xvalue。在您的示例中,fun().v接近其生命周期的结束。因此,它的资源可以被移动。由于它的资源可以被移动,它不是lvalue,因此您的表达式符合仅剩下的叶值类别 - xvalue

2

编辑:好吧,我想我终于从标准中找到了一些内容:

请注意,v 的类型为 int,其具有内置的赋值运算符:

13.3.1.2 表达式中的运算符

4 对于内置赋值运算符,左操作数的转换受以下限制: —— 不引入临时变量来保存左操作数,且[...]

fun1() 应该返回一个引用。函数的非引用/指针返回类型是一个 r 值。

3.10 左值和右值

5 调用不返回左值引用的函数的结果是一个 r 值[...]

因此,fun1().v 是一个 r 值。

8.3.2 引用

2 使用 & 声明的引用类型称为左值引用, 使用 && 声明的引用类型称为 r 值引用。 左值引用和 r 值引用是不同的类型。


什么?他应该返回一个临时引用吗? - GManNickG
毫无疑问,根据3.10章节,fun1()是一个右值,但请阅读问题。表达式fun1().v不是调用函数的结果。它被解析为(fun1()).x - 成员访问。 - MSalters
@MSalters:编辑了帖子,我认为这会有所帮助。 - dirkgently
@MSalters:有没有理由相信,对于一个聚合类型的rvalue对象,它的成员可以是lvalue? - dirkgently
@dribeas:在你的回答中?听起来足够接近了。C&V,请,找不到它:( 另外,值得一提的是,永远不存在rvalue-to-lvalue转换(尽管lvalue-to-rvalue转换是合法的)。 - dirkgently
显示剩余4条评论

0

我注意到gcc在赋值表达式中很少对rvalue作为lvalue使用而感到犹豫。例如,以下代码可以编译通过:

class A {
};

extern A f();

void g()
{
   A myA;
   f() = myA;
}

为什么这个是合法的,而这个不是(即它不能编译),真的让我很困惑:

extern int f();

void g()
{
   f() = 5;
}

个人认为,标准委员会需要就左值、右值及其使用范围进行解释。 这也是我对右值相关问题如此感兴趣的原因之一。


不是的,但我可以看到有人假设由于operator=是一个成员函数(即使它执行修改),因此应该允许在lvalue上调用它。 class X { void m(); }; X f(); void h() { f().m() } 是有效的代码片段。对我来说,问题在于即使这是真的,在lvalue上允许调用operator=将与原始类型不一致(标准很清楚),因此如果我要进行调用,我不会允许它。毕竟,即使operator=是一个成员函数,它也是_特殊的_。 - David Rodríguez - dribeas
@Omnifarious - 我认为你的代码展示了为什么通常建议返回“const A”,因为这样原始类型和用户定义类型之间就没有差异。这通常在运算符重载的上下文中推荐使用(例如,使operator+(T, T)返回const T),但我认为它适用于任何返回用户定义类型的函数。 - Manuel
@Manuael:我曾经同意你的看法,但是在C++0x中使用rvalue引用,这会导致什么情况?我希望返回值能够被用作非const rvalue引用。 - Omnifarious
实际上,我没有看到这个例子有任何问题。 对于类类型来说,赋值是通过成员运算符 = 进行的。如果没有用户定义的 operator=,则会生成一个默认的operator=。由于成员函数调用不需要 lvalue,因此在类类型上执行赋值总是可以的,除非明确禁止使用私有 operator=。 而 int 是内置类型,因此没有成员运算符。 - hpsMouse
@DavidRodríguez-dribeas:“对我来说问题在于,即使那是真的,允许在左值上调用operator=将证明与原始类型不一致。” 那+=呢?同样的道理吗? - curiousguy
显示剩余2条评论

0

当你考虑到编译器会为你生成默认构造函数、默认复制构造函数和默认复制赋值运算符(如果你的结构体/类不包含引用成员),这一点就变得很明显了。然后,想想标准允许你在临时对象上调用成员方法,也就是说,你可以在非const临时对象上调用非const成员。

看看这个例子:

struct Foo {};
Foo foo () {
    return Foo();
}

struct Bar {
private:
    Bar& operator = (Bar const &); // forbid
};
Bar bar () {
    return Bar();
}
int main () {
    foo() = Foo(); // okay, called operator=() on non-const temporarie
    bar() = Bar(); // error, Bar::operator= is private
}

如果你写下了

struct Foo {};
const Foo foo () { // return a const value
    return Foo();
}

int main () {
    foo() = Foo(); // error
}

例如,如果您让函数foo()返回一个const临时变量,则会出现编译错误。

为了使示例完整,以下是如何调用const临时变量的成员:

struct Foo {
    int bar () const { return 0xFEED; }
    int frob ()      { return 0xFEED; }
};
const Foo foo () {
    return Foo();
}

int main () {
    foo().bar(); // okay, called const member method
    foo().frob(); // error, called non-const member of const temporary
}

你可以将临时变量的生命周期定义为当前表达式内。这也是为什么你可以修改成员变量的原因;如果不能修改,那么调用非const成员方法的可能性就会变得荒谬。

编辑:下面是必要的引用:

12.2 临时对象:

  • 3) [...] 临时对象在评估它们创建的点所在的包含全表达式(1.9)的最后一步被销毁。[...]

然后(或者更好的是,在此之前)

3.10 左值和右值:

  • 10) 除非在某些情况下可以用类类型的rvalue修改其引用对象,否则需要一个对象的左值才能对其进行修改。[例子:对于一个对象(9.3)调用的成员函数可以修改该对象。]

并附上一个示例使用:http://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Named_Parameter


我认为你可能有和我一样的困惑。请看我对这个答案的评论:https://dev59.com/6XE95IYBdhLWcg3wp_gg#2220281 - Manuel
@Manuel:说实话,我不明白你为什么认为我很困惑!? - Sebastian Mach
@phreshel - 请阅读我链接的评论以及dribeas的回复。您的推理是正确的,但在这里并不适用,因为v是一种原始类型。 - Manuel
当我思考这个问题时,它变得很有趣。 对rvalue对象进行成员函数调用是允许的,并且为了调用成员函数,对象必须绑定到“this”指针,这会在函数调用期间将对象转换为lvalue。这意味着rvalue对象有时可以变成lvalue。 但是成员变量访问略有不同,并未被标准提及。 顺便说一句,我讨厌“在某些情况下”的说法。 :-) - hpsMouse

-2

你的代码没有情景。返回的结构体是在栈上分配的,所以赋值结果会立即丢失。

你的函数应该通过以下方式之一分配 A 的新实例:

new A()

在这种情况下,更好的签名

A* f(){ ...

或者返回现有实例,例如:

static A globalInstance;
A& f(){ 
  return globalInstance;
}

2
“has no scene”是什么意思? - nobody
你的评论没有意义。 - Sebastian Mach
@Andrew Medico - 当函数返回堆栈分配的值时,该值应存储在某处。否则,您的赋值将会丢失。这就是为什么g++将此情况视为无意义的原因。在我的帖子中,我已经解释了为什么。 - Dewfy
@phresnel (Foo().x=5).member() 假设只有被重载的 'operator =' 返回 'struct A' 的引用,而不是 int,在这种情况下,g++ 不会产生警告。 - Dewfy
@Dewfy:抱歉格式不好,如果你将代码粘贴在“”中,就会看得更清楚。 - Sebastian Mach
显示剩余5条评论

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