29

返回结构的函数调用是一个右值表达式,但它的成员呢?
这段代码在我的 g++ 编译器中运行良好,但 gcc 给出错误提示“需要左值作为赋值的左操作数”:

struct A
{
    int v;
};

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

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

gcc 将fun().v其视为右值,我可以理解这一点。
但是 g++ 并不认为赋值表达式是错误的。这是否意味着 fun1().v 在 C++ 中是左值?
现在的问题是,我搜索了 C++98/03 标准,没有发现任何关于fun().v是左值还是右值的信息。
那么,它是什么?

4

6 回答 6

16

右值表达式的成员是右值。

该标准在 5.3.5 [expr.ref] 中规定:

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

于 2010-02-08T12:20:31.767 回答
2

编辑:好的,我想我终于从标准中得到了一些东西:

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

13.3.1.2 表达式中的运算符

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

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

3.10 左值和右值

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

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

8.3.2 参考文献

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

于 2010-02-08T08:12:33.703 回答
2

这是了解什么是xvaluesglvalues的好时机。

右值可以有两种类型 -右xvalues。根据新的 C++17 标准

prvalue是一个表达式,它的求值初始化一个对象、位域或运算符的操作数,由它出现的上下文指定。

所以像fun()你的例子中的东西评估为一个纯右值(这是一个右值)。这也告诉我们这fun().v不是纯右值,因为它不是普通的初始化。

也是右值的Xvalue定义如下

一个xvalue(一个“eXpiring”值)也指一个对象,通常接近其生命周期的末尾(例如,它的资源可能会被移动)。某些涉及右值引用(8.3.2)的表达式产生xvalues。[ 示例:调用返回类型为对对象类型的右值引用的函数的结果是 xvalue (5.2.2)。- 结束示例]

除了 rvalues 之外,另一个总括值类别是glvalue,它有两种类型xvalues和传统的lvalues

至此,我们已经定义了基本价值类别。这可以像这样可视化

在此处输入图像描述

泛泛地认为,glvalue类别表示值在移动语义成为事物之前应该表示的含义——一种可以在表达式左侧的事物。 glvalue表示广义左值。

如果我们看一下xvalue的定义,那么如果某物接近其生命周期的尽头,它就会说它是一个xvalue 。在您的示例中,fun().v它的生命周期即将结束。所以它的资源可以移动。并且由于它的资源可以移动,它不是左值,因此您的表达式适合唯一剩下的叶值类别 - 一个xvalue

于 2017-04-04T02:36:01.423 回答
0

我注意到 gcc 往往对在赋值表达式中使用右值作为左值很少感到内疚。例如,这编译得很好:

class A {
};

extern A f();

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

为什么这是合法的而这不是(即它不编译)虽然真的让我感到困惑:

extern int f();

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

恕我直言,标准委员会对左值、右值以及它们可以在哪里使用有一些解释。这是我对这个关于 rvalues的问题如此感兴趣的原因之一。

于 2010-02-08T08:19:59.200 回答
0

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

看这个例子:

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 temporarie 的成员:

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) 对象的左值是修改对象所必需的,但在某些情况下也可以使用类类型的右值来修改其所指对象。[示例:为对象(9.3)调用的成员函数可以修改该对象。]

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

于 2010-02-08T09:52:06.230 回答
-2

你的代码没有场景。返回的结构在堆栈上分配,因此分配结果会立即丢失。

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

new A()

在这种情况下更好的签名

A* f(){ ...

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

static A globalInstance;
A& f(){ 
  return globalInstance;
}
于 2010-02-08T08:16:42.500 回答