为什么我可以将返回值的函数结果赋值给一个变量?它是一个左值吗?

86

考虑以下代码:

struct foo
{
  int a;
};

foo q() { foo f; f.a =4; return f;}

int main()
{
  foo i;
  i.a = 5;
  q() = i;
}

没有编译器会对此抱怨,甚至包括Clang。为什么 q() = ... 这一行是正确的?


注:该段内容已经是中文,无需翻译。

5
您认为这段代码有什么问题?q()返回一个结构体,然后您给它赋了一个值。这有什么问题吗? 问题在于q()函数返回的是结构体类型的值,而结构体是值类型。这意味着当您将其赋值给另一个变量时,将创建结构体的一个副本,而不是引用原来的结构体。因此,在对副本进行更改时,原始结构体不会受到影响。 - Andy Johnson
3
@Andy:我认为将值分配给返回值通常是容易出错的(除非operator=正在进行某些可能是不良设计实践的魔法操作)。我本来希望这种情况会像不使用局部变量那样发出警告。 - John
2
@Andy Johnson:普遍的误解是人们认为赋值左侧必须是一个lvalue。在这种情况下,似乎违反了这个要求。但实际上并没有这样的要求。内置赋值确实需要LHS上的lvalue,但重载赋值不需要。在这种情况下,我们正在处理重载赋值,尽管这并不明显。 - AnT stands with Russia
4个回答

79

如果一个函数的返回值是引用,那么它就是一个l-value(C++03)(5.2.2 [expr.call] / 10)

如果返回类型是基本类型,则会导致编译错误。(5.17 [expr.ass] / 1)

这种情况之所以能够起作用是因为你可以在类类型的r-values上调用成员函数(即使是非const成员函数),而将foo赋值给它是一个实现定义的成员函数:foo& foo::operator=(const foo&)。条款5中的运算符限制仅适用于内置运算符(5 [expr] / 3),如果重载分辨率选择了运算符的重载函数调用,则应使用该函数调用的限制。

这就是为什么有时候建议返回类类型的对象作为const对象(例如const foo q();),但这可能会对C++0x产生负面影响,因为它可能会阻碍移动语义的正常工作。


20
以具体示例来说,考虑 std::cout << "Hello, world" << std::endl。第一个 operator<<() 返回一个左值,你可以在它上面调用第二个 operator<<()。因此,在返回值上调用方法的这种做法比许多人想象的更为普遍。 - MSalters
5
这让我想起了《C++设计与演化》一书中的一句话:“这就是一个观念的起源,它逐渐发展成为C++设计的经验法则:用户定义的类型和内置类型应该在语言规则方面表现相同,并且应该获得语言及其相关工具的同等支持程度。当这个理想被提出时,内置类型获得了最好的支持,但是C++已经超过了这个目标,以至于与用户定义的类型相比,内置类型现在接收到稍微劣一些的支持。” - Seth Carnegie
12
@MSalters 是的,但在 cout 的情况下,它实际上返回一个左值(它返回自身和引用 *this),而不是一个按值传递的东西。所以稍有不同。 - Seth Carnegie
6
@MSalters:ostream::operator<<和operator=都返回引用,这是一个左值。 - John
5
std::cout << "duh" << std::endl中,所有的operator<<都返回非const引用(即左值),因为这些操作符都要求它们的第一个参数是非const引用,所以原始的std::ostream对象不能是右值。在其他例子中(我认为这是问题的动机),临时对象上调用了非const函数。 - James Kanze
显示剩余3条评论

9
因为结构体可以被赋值,而你的q()返回struct foo的副本,所以它将返回的结构体赋给了提供的值。
在这种情况下,这并没有实际作用,因为结构体随后就会超出范围,并且你一开始也没有保留对它的引用,所以你无论如何都不能对其进行任何操作(在这个特定的代码中)。
这样做更有意义(尽管仍不是最佳实践)。
struct foo
{
  int a;
};

foo* q() { foo *f = new malloc(sizeof(foo)); f->a = 4; return f; }

int main()
{
  foo i;
  i.a = 5;

  //sets the contents of the newly created foo
  //to the contents of your i variable
  (*(q())) = i;
}

7
int blah() { int f = 4; return f; } int main() { int a = 99; blah() = a; }这段代码有什么区别吗?是结构体的神奇之处吗?因为这段代码无法编译。 - Seth Carnegie
@Seth 请查看 Charles Bailey 的答案。 - Chad
3
Int 类型没有成员函数,所以它们也没有 int::operator=(int)。这就是结构体的“神奇”之处:它们有默认方法。 - MSalters
@Seth,因为在你的例子中函数返回一个int而不是一个对象,所以你不能给它赋值。如果函数返回一个int的引用(int&),那么你就可以给它赋值了。 - bruno
2
@bruno: s/nothing/anything/ - Lightness Races in Orbit

8

这个技术的一个有趣应用:

void f(const std::string& x);
std::string g() { return "<tag>"; }

...

f(g() += "</tag>");

在这里,g() +=修改了临时变量,这可能比使用+创建额外的临时变量更快,因为分配给g()返回值的堆已经有足够的空余容量来容纳</tag>
请参见在ideone.com上使用GCC / C++11运行
现在,哪位计算机新手提到了优化和邪恶...?;-]。

0
除了其他很好的答案之外,我想指出std::tie可以在另一个函数中解包数据。在这里看看。因此,它本身并不容易出错,只需记住它可能是一种有用的设计模式即可。

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