为什么非 const 引用不能绑定到临时对象?

255

为什么不允许获取非const引用到临时对象,这是由函数getx()返回的? 显然,C++标准禁止这样做,但我对此限制的目的感兴趣,而不是参考标准。

struct X
{
    X& ref() { return *this; }
};

X getx() { return X();}

void g(X & x) {}    

int f()
{
    const X& x = getx(); // OK
    X& x = getx(); // error
    X& x = getx().ref(); // OK
    g(getx()); //error
    g(getx().ref()); //OK
    return 0;
}
  1. 很明显,对象的生命周期不可能是原因,因为C++标准并没有禁止对常量对象进行引用。
  2. 在上面的示例中,临时对象显然不是常量,因为可以调用非常量函数。例如,ref() 可能会修改临时对象。
  3. 此外,ref() 允许你欺骗编译器,并获得对临时对象的引用,从而解决了我们的问题。

另外:

他们说“将临时对象分配给const引用可以延长该对象的生命周期”,但“对于非const引用则没有任何说明”。 我的附加问题是:以下赋值是否延长了临时对象的生命周期?

X& x = getx().ref(); // OK

4
我不同意“对象的生命周期不能是原因”的观点,仅仅因为标准规定,将临时对象赋给const引用会将该对象的生命周期延长到const引用的生命周期。但是对于非const引用,则没有任何说明... - SadSido
3
“关于‘没提到非常量引用…’的原因是什么呢?这是我的问题的一部分。这有任何意义吗?或许标准的作者们只是忘了非常量引用,很快我们就会看到下一个核心问题吗?” - Alexey Malistov
3
GotW #88: “最重要的 const”候选者在这篇文章中,作者Herb Sutter讨论了C++中const关键字的一个重要用途:作为函数参数类型声明的一部分。他介绍了一个例子来说明如何使用const来传递对象的只读引用,并解释了const在这种情况下的优点。此外,他还讨论了如何使用const成员函数来保护对象的状态,并提出了一些有关const的最佳实践建议。总之,这篇文章强调了const关键字在C++中的重要性和灵活性,并提供了一些实用的示例和指导。 - Fernando N.
5
@Michael:VC将rvalue绑定到非const引用。他们称之为一种特性,但实际上它是一个错误。请注意,这不是因为本质上不合逻辑而出现的bug,而是因为明确地禁止它以防止愚蠢的错误。 - sbi
7
Herb Sutter 的 GotW #88:候选 "最重要的 const" 已经移动。 - sarnold
显示剩余7条评论
11个回答

113
从这篇关于rvalue引用的Visual C++博客文章中:
引用: ... C++不希望你意外修改临时对象,但是直接在可修改的rvalue上调用非const成员函数是明确的,所以是允许的...
基本上,你不应该尝试修改临时对象,因为它们是临时的对象,随时会消失。你被允许调用非const方法的原因是,嗯,只要你知道自己在做什么并且明确表达出来(比如使用reinterpret_cast),你可以做一些“愚蠢”的事情。但是如果你将临时对象绑定到非const引用上,你可以一直传递它,只是为了让你对对象的操作消失,因为在某个地方你完全忘记了这是一个临时对象。
如果我是你,我会重新考虑我的函数设计。为什么g()接受引用?它是否修改了参数?如果没有,将其改为const引用;如果是,为什么要尝试将临时对象传递给它?难道你不在意你正在修改一个临时对象吗?为什么getx()返回临时对象?如果你与我们分享你的真实场景和你想要实现的目标,你可能会得到一些很好的建议。
违背语言规则并愚弄编译器很少解决问题,通常只会制造问题。
  1. X& x = getx().ref(); // OK when will x die? - 我不知道也不在乎,因为这正是我所说的“违背语言”的意思。语言规定“临时对象在语句结束时销毁,除非它们被绑定到const引用,这样它们会在引用超出作用域时销毁”。根据这个规则,似乎x在下一条语句开始时就已经消失了,因为它没有被绑定到const引用(编译器不知道ref()返回什么)。不过这只是一个猜测。

  2. 我已经清楚地表明了目的:你不能修改临时对象,因为这毫无意义(忽略C++0x的右值引用)。关于“为什么我可以调用非const成员函数?”这个问题,我没有比我上面已经给出的更好的答案。

  3. 嗯,如果我对X& x = getx().ref();中的x在语句结束时消失的猜测是正确的,问题就很明显了。

无论如何,根据你的问题和评论,我觉得即使有这些额外的答案也无法满足你。这是最后的尝试/总结:C++委员会决定修改临时对象是没有意义的,因此,他们禁止将其绑定到非const引用。可能还涉及一些编译器实现或历史问题,我不清楚。然后,出现了一些特殊情况,并决定不顾一切地允许通过调用非const方法直接修改。但这是个例外 - 通常是不允许修改临时对象的。是的,C++经常很奇怪。

5
实际上,您可以修改rvalue(临时值)。这在内置类型(例如int)中是被禁止的,但对于用户自定义类型是允许的,例如(std::string("A")+"B").append("C") - sbi
7
Stroustrup在《D&E》中禁止将rvalue绑定到非const引用的原因是,如果Alexey的函数g()修改了对象(你会从一个取非const引用的函数期望这样做),它将修改一个即将死亡的对象,所以无论如何,都没有人可以访问修改后的值。他说这很可能是一个错误。 - sbi
5
我同意sbi的看法 - 这件事情一点也不是吹毛求疵。它涉及到移动语义的基础,即类类型的右值最好保持非const。 - Johannes Schaub - litb
2
@sbk,你说临时变量在包含它们的语句结束之前一直存在,这意味着以下for循环是正确的:for(char const *s = string().c_str();*s;s++) ;。但是这是错误的,因为"string()"不是在语句结束时被销毁,而是在初始化s结束时被销毁,从而使循环解引用未拥有的内存。 - Johannes Schaub - litb
2
“C++委员会决定修改临时变量是没有意义的”是错误的。 - curiousguy
显示剩余15条评论

47

在您的代码中,getx()返回一个临时对象,即所谓的“rvalue”。 您可以将rvalues复制到对象(也称为变量)中或将它们绑定到const引用(这将延长其生命周期直到引用结束)。 您无法将rvalues绑定到非const引用。

这是一项有意的设计决策,旨在防止用户意外修改即将在表达式结束时消失的对象:

g(getx()); // g() would modify an object without anyone being able to observe

如果你想这样做,你需要先将对象拷贝到本地或将其绑定到一个常量引用:

X x1 = getx();
const X& x2 = getx(); // extend lifetime of temporary to lifetime of const reference

g(x1); // fine
g(x2); // can't bind a const reference to a non-const reference

请注意,下一个 C++ 标准将包含右值引用。因此,你所知道的引用现在将被称为“左值引用”。你将允许将右值绑定到右值引用,并且你可以根据“右值性”重载函数:
void g(X&);   // #1, takes an ordinary (lvalue) reference
void g(X&&);  // #2, takes an rvalue reference

X x; 
g(x);      // calls #1
g(getx()); // calls #2
g(X());    // calls #2, too
右值引用的理念在于,由于这些对象即将死亡,您可以利用这一点知识并实现所谓的“移动语义”,一种特定的优化:
class X {
  X(X&& rhs)
    : pimpl( rhs.pimpl ) // steal rhs' data...
  {
    rhs.pimpl = NULL; // ...and leave it empty, but deconstructible
  }

  data* pimpl; // you would use a smart ptr, of course
};


X x(getx()); // x will steal the rvalue's data, leaving the temporary object empty

2
嗨,这是一个很棒的答案。需要知道一件事,g(getx())不起作用,因为它的签名是g(X& x),而get(x)返回一个临时对象,所以我们不能将临时对象(rvalue)绑定到非常量引用上,对吗?在你的第一段代码中,我认为应该是const X& x2 = getx();而不是const X& x1 = getx(); - SexyBeast
1
感谢您指出我写下答案后五年中的错误! :-/ 是的,您的推理是正确的,尽管有点颠倒了:我们不能将临时对象绑定到非const(左值)引用,因此由getx()返回的临时对象(而不是get(x))不能被绑定到作为g()参数的左值引用。 - sbi
嗯,你是什么意思说 getx()(而不是 get(x) - SexyBeast
当我写“...getx()(而不是get(x))...”时,我的意思是函数的名称是getx(),而不是get(x)(就像你所写的那样)。 - sbi
这个答案混淆了术语。rvalue 是一个表达式类别。临时对象是一个对象。rvalue 可能表示临时对象,也可能不表示;临时对象可能被 rvalue 表示,也可能不被表示。 - M.M

19
你展示的是允许使用操作符链。
 X& x = getx().ref(); // OK

表达式是'getx().ref();',在分配给'x'之前执行完毕。
请注意,getx()不返回引用,而是将完整的对象返回到本地上下文中。该对象是临时的,但它是const,因此允许您调用其他方法来计算值或发生其他副作用。
// It would allow things like this.
getPipeline().procInstr(1).procInstr(2).procInstr(3);

// or more commonly
std::cout << getManiplator() << 5;

请查看本答案结尾,以获取更好的示例

由于绑定临时对象会生成指向在表达式结束时将被销毁的对象的引用,因此您不能将临时对象绑定到引用上(这会留下一个悬空引用,不整洁,而且标准不喜欢不整洁)。

ref()返回的值是一个有效的引用,但该方法不关注其返回的对象的生命周期(因为它无法在其上下文中获得该信息)。您基本上只是做了以下等价操作:

x& = const_cast<x&>(getX());

使用对临时对象的const引用进行此操作是可以的,原因在于标准将临时对象的寿命延长到引用的寿命之外,因此临时对象的寿命得以延长到语句结束之后。那么唯一剩下的问题就是为什么标准不希望允许对临时对象的引用将对象的生命周期延长到语句结束之后呢?我认为这是因为这样做会使编译器很难处理临时对象。对于对临时对象的const引用,这样做是有限制的,并迫使你复制对象以执行任何有用的操作,但确实提供了一些有限的功能。想象一下这种情况:
int getI() { return 5;}
int x& = getI();

x++; // Note x is an alias to a variable. What variable are you updating.

延长这个临时对象的寿命会非常混乱。
相反,下面的内容:

int const& y = getI();

我将为您提供易于使用和理解的代码。

如果您想修改值,应该将该值返回到变量中。如果您试图避免从函数中复制对象的成本(似乎对象是通过复制构造返回的(技术上是这样的)),那么不要担心,编译器非常擅长'返回值优化'


3
那么现在唯一剩下的问题是,为什么标准不允许引用暂时对象来延长对象的生命周期超过语句结束?你理解了我的问题。但我不同意你的观点。你说“使编译器变得非常困难”,但这已经对const引用实现了。你在示例中说,“请注意x是一个变量的别名。你正在更新哪个变量?”没有问题。有唯一的变量(临时对象)。某些临时对象(等于5)必须被更改。 - Alexey Malistov
1
@Alexey:请注意,将其绑定到const引用以增强临时对象的生命周期是一个特意添加的例外(据我所知,这是为了允许手动优化)。并没有为非const引用添加例外,因为将临时对象绑定到非const引用被认为很可能是程序员的错误。 - sbi
1
@alexy:对一个不可见的变量的引用!不是那么直观。 - Martin York
@Loki:这是你最新的评论。我不确定为什么非自动绑定的临时变量扩展是相关的,因为已经允许将成员const引用绑定到构造函数参数const引用,该引用又绑定到一个临时变量,这会创建一个生命周期问题。添加非const引用不会在这方面创建新问题,它只会扩大现有陷阱的范围。 - Ben Voigt
@BenVoigt:那个日期是 Oct 14 '09 at 18:04 的。 - Martin York
显示剩余8条评论

16

为什么C++ FAQ中讨论,其中加粗部分是我的:

在C++中,非常量引用可以绑定到左值,而常量引用可以绑定到左值或右值,但没有任何东西可以绑定到非常量右值。这是为了防止人们更改将在使用其新值之前销毁的临时对象的值。例如:

void incr(int& a) { ++a; }
int i = 0;
incr(i);    // i becomes 1
incr(0);    // error: 0 is not an lvalue

如果允许对incr(0)进行操作,要么是一些从未被使用的临时变量被增加了,要么更糟糕的情况是0的值会变成1。虽然后者听起来有些傻,但早期Fortran编译器中实际上就存在这样的一个错误,为保存值0而分配了一个内存位置。


1
看到那个被Fortran“零错误”咬了的程序员的表情一定很有趣! x * 0 竟然等于 x?什么?什么? - John D
2
那个最后的参数特别弱。值得一提的编译器永远不会实际将0的值改变为1,甚至以这种方式解释 incr(0);。显然,如果允许这样做,它将被解释为创建一个临时整数并将其传递给 incr() - user3204459
1
这是正确的答案。当涉及到隐式转换时,这个丢失副作用的问题会变得更加严重。例如,假设你将 incr(int& a) 改为 incr(long& a)。现在表达式 incr(i)i 转换为一个临时的 long 并通过引用传递它。incr 内部的修改现在对调用者没有影响。这将非常令人困惑。这个问题在 Howard Hinnant 的原始移动语义提案中进行了讨论:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n1377.htm#Binding%20temporaries%20to%20references - Jack O'Connor
常量引用可以避免这个特定问题,因为你不能通过花招写入常量引用。但是它们仍然可能导致其他相关问题。例如,如果你返回了给定的常量引用,则返回引用的有效生命周期取决于参数是左值(在这种情况下,它在某些范围内有效)还是临时右值(在这种情况下,它在语句结束时失效)。这可能会像上面的情况一样悄悄地改变。 - Jack O'Connor

6
主要问题是:
g(getx()); //error

这是一个逻辑错误:g 修改了 getx() 的结果,但你没有任何机会检查修改后的对象。如果 g 不需要修改其参数,则不需要使用左值引用,可以通过值或常量引用来获取参数。

const X& x = getx(); // OK

“is valid” 是因为有时您需要重复使用表达式的结果,而且很明显您正在处理一个临时对象。

然而,“无法制作”

X& x = getx(); // error

有效,而不需要使g(getx())有效,这正是语言设计者一开始试图避免的。

g(getx().ref()); //OK

这是有效的,因为方法只知道this的const性质,不知道它们是在lvalue还是rvalue上调用。

像往常一样,在C++中,您可以绕过此规则,但必须明确告诉编译器您知道自己在做什么:

g(const_cast<x&>(getX()));

6
似乎原始问题“为什么不允许这样做”已经被清楚地回答了:“因为这很可能是一个错误”。
顺便说一句,我想展示一下“如何”做到这一点,尽管我不认为这是一种好的技术。
我有时想将临时变量传递给一个非const引用的方法,是为了有意地丢弃由引用返回的值,而调用方法并不关心这个值。类似这样的:
// Assuming: void Person::GetNameAndAddr(std::string &name, std::string &addr);
string name;
person.GetNameAndAddr(name, string()); // don't care about addr

正如之前的回答所解释的那样,这段代码无法编译。但是这段代码可以被编译并且在我的编译器上可以正常工作:

person.GetNameAndAddr(name,
    const_cast<string &>(static_cast<const string &>(string())));

这只是说明你可以使用强制类型转换来欺骗编译器。显然,声明并传递一个未使用的自动变量会更清晰:
string name;
string unused;
person.GetNameAndAddr(name, unused); // don't care about addr

这种技术确实引入了一个不必要的本地变量到方法的作用域中。如果出于某种原因您想要防止它在方法后面被使用,例如为了避免混淆或错误,您可以将其隐藏在一个本地块中:

string name;
{
    string unused;
    person.GetNameAndAddr(name, unused); // don't care about addr
}

-- Chris(克里斯)

不错的例子。这个能够工作的事实表明,错误并不是由于C++的逻辑限制而产生的,而是一种设计决策。 - c z

4
优秀的问题,这是我对更简洁答案的尝试(因为很多有用的信息在评论中,很难从噪音中挖掘出来)。任何直接绑定到临时对象的引用都会延长其生命周期[12.2.5]。另一方面,使用另一个引用初始化的引用不会延长生命周期(即使最终是同一个临时对象)。这是有道理的(编译器不知道该引用最终指向什么)。但整个想法非常混乱。例如,const X &x = X();将使临时对象的寿命与x引用一样长,但const X &x = X().ref();不会(谁知道ref()实际返回了什么)。在后一种情况下,X的析构函数在此行结束时被调用。(具有非平凡析构函数的对象可以观察到这一点。)因此,它似乎普遍令人困惑和危险(为什么要复杂化关于对象生命周期的规则?),但是可能至少对于const引用有所需求,因此标准确实为它们设置了这种行为。

[来自sbi的评论]: 需要注意的是,将临时对象绑定到const引用上以增强其生命周期是有意添加的例外情况(据我所知,这是为了允许手动优化)。对于非const引用没有添加例外情况,因为将临时对象绑定到非const引用被视为可能是程序员的错误。

所有的临时对象都会持续到完整表达式结束。然而,要利用它们,你需要像使用ref()那样的技巧。这是合法的。除了提醒程序员正在发生一些不寻常的事情(即修改将很快丢失的引用参数),似乎没有什么好理由去跳过额外的步骤。

[另一个sbi评论] Stroustrup在《D&E》中禁止将rvalue绑定到非const引用的原因是,如果Alexey的g()函数修改对象(从接受非const引用的函数中可以预期),它将修改即将死亡的对象,因此无论如何都无法获取修改后的值。他说这很可能是一个错误。


3

你为什么需要X& x = getx();呢?直接使用X x = getx();并依赖于RVO即可。


3
因为我想调用 g(getx()) 而不是 g(getx().ref()) - Alexey Malistov
4
@Alexey,那不是一个真正的理由。如果你那样做,那么你就会有一个逻辑错误,因为 g 将会修改你无法再接触到的某些东西。 - Johannes Schaub - litb
3
@JohannesSchaub-litb 或许他不在意。 - curiousguy
“_rely on RVO_” 除了不叫“RVO”之外,其他内容都翻译过来了。 - curiousguy
2
@curiousguy:这是一个非常被接受的术语。用“RVO”来指代它完全没有任何问题。 - Puppy

3
邪恶的解决方法涉及“mutable”关键字。实际上,如何做到邪恶留给读者自己去练习。或者请参阅此处:http://www.ddj.com/cpp/184403758

1

我有一个场景想要分享,其中我希望我能像Alexey所要求的那样做。在一个Maya C++插件中,我必须执行以下策略,以便将值放入节点属性:

MFnDoubleArrayData myArrayData;
MObject myArrayObj = myArrayData.create(myArray);   
MPlug myPlug = myNode.findPlug(attributeName);
myPlug.setValue(myArrayObj);

这很繁琐,因此我编写了以下辅助函数:
MPlug operator | (MFnDependencyNode& node, MObject& attribute){
    MStatus status;
    MPlug returnValue = node.findPlug(attribute, &status);
    return returnValue;
}

void operator << (MPlug& plug, MDoubleArray& doubleArray){
    MStatus status;
    MFnDoubleArrayData doubleArrayData;
    MObject doubleArrayObject = doubleArrayData.create(doubleArray, &status);
    status = plug.setValue(doubleArrayObject);
}

现在我可以将文章开头的代码写成:

(myNode | attributeName) << myArray;

问题在于它在Visual C++之外无法编译,因为它试图将从|运算符返回的临时变量绑定到<<运算符的MPlug引用上。我希望它是一个引用,因为这段代码被调用很多次,我不想让MPlug被频繁复制。我只需要临时对象存活到第二个函数结束即可。
好吧,这就是我的情况。只是想展示一个例子,其中一个人想做Alexey描述的事情。欢迎所有的批评和建议!
谢谢。

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