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


226

为什么不允许获得对一个临时对象的非常量引用,哪个函数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 ++ Standard 不禁止对对象的常量引用。
  2. 显然,上面的示例中的临时对象不是常量,因为允许调用非常量函数。例如,ref()可以修改临时对象。
  3. 另外,ref()允许您欺骗编译器并获得指向该临时对象的链接,从而解决了我们的问题。

此外:

他们说“将临时对象分配给const引用可延长该对象的寿命”,“关于非const引用却一无所获”。我还有一个问题。以下分配是否会延长临时对象的寿命?

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

4
我不同意“对象的生存期不能成为原因”部分,仅因为标准中已说明,将临时对象分配给const引用会将该对象的生存期扩展到const引用的生存期。尽管没有关于非const引用的任何消息……
SadSido

3
好吧,“尽管没有说什么非常量引用……”的原因是什么。这是我的问题的一部分。这有什么意义吗?也许Standard的作者只是忘记了非const引用,不久我们将看到下一个Core Issue?
阿列克谢·马里斯托夫

2
GotW#88:“最重要的const”的候选人。herbutter.spaces.live.com/blog/cns!2D4327CC297151BB!378.entry
fnieto-Fernando Nieto

4
@Michael:VC将右值绑定到非常量引用。他们称此为功能,但确实是一个错误。(请注意,这不是错误,因为它本来就不合逻辑,而是因为明确排除了它以防止愚蠢的错误。)
sbi

Answers:


97

在此Visual C ++博客文章中,有关右值引用

... C ++不想让您不小心修改临时变量,但是直接在可修改的rvalue上调用非常量成员函数是显式的,因此允许...

基本上,您不应该因为临时对象是临时对象并且会在任何时候死掉的原因而尝试修改它们。允许您调用非const方法的原因是,只要您知道自己在做什么并且对它很明确,就可以做一些“愚蠢”的事情(例如,使用reinterpret_cast)。但是,如果您将一个临时对象绑定到一个非const引用,则可以继续将它永久传递给“永久”对象,以使对对象的操作消失,因为在您完全忘记的情况下,这是一个临时对象。

如果我是你,我会重新考虑我的功能设计。为什么g()接受引用,它会修改参数吗?如果不是,请使其成为const引用,如果是,请为什么尝试将临时变量传递给它,您不在乎它是您要修改的临时变量吗?为什么getx()无论如何都会返回临时值?如果您与我们分享您的真实情况以及您想要实现的目标,则可能会得到一些很好的建议。

违反语言并欺骗编译器很少解决问题-通常会产生问题。


编辑:解决评论中的问题:1)X& x = getx().ref(); // OK when will x die?-我不知道,我不在乎,因为这正是我“违背语言”的意思。该语言说:“除非声明绑定到const引用,否则临时语句会在语句末尾消失,在这种情况下,当引用超出范围时它们就会死亡”。应用该规则,似乎x在下一条语句的开头已经死了,因为它没有绑定到const引用(编译器不知道ref()返回什么)。但是,这只是一个猜测。

2)我清楚地说明了目的:不允许修改临时对象,因为这样做没有意义(忽略C ++ 0x rvalue引用)。问题“那为什么允许我打电话给非const成员?” 是一个不错的选择,但是我没有比我上面已经说过的更好的答案了。

3)好吧,如果我X& x = getx().ref();对语句结尾处的x 死是正确的,那么问题就显而易见了。

无论如何,根据您的问题和评论,我认为即使这些额外的答案也无法满足您。这是最后的尝试/摘要:C ++委员会认为修改临时文件没有意义,因此,它们不允许绑定到非const引用。我不知道,可能是某些编译器实现或涉及历史性问题。然后,出现了一些特定情况,并决定在各种情况下,它们仍将允许通过调用非const方法直接进行修改。但这是一个例外-通常不允许您修改临时文件。是的,C ++通常很奇怪。


2
@sbk:1)实际上,正确的短语是:“ ...在完整表达式的末尾...”。我认为,“完整表达式”被定义为不是其他某些表达式的子表达式。我不确定这是否始终与“声明的结尾”相同。
sbi

5
@sbk:2)其实,你允许修改右值(临时对象)。内置类型(int等)禁止使用,但用户定义的类型则允许使用(std::string("A")+"B").append("C")
sbi

6
@sbk:3)Stroustrup给出(在D&E中)不允许将rvalue绑定到非const引用的原因是,如果Alexey g()修改了对象(您希望从使用非const引用的函数中得到该对象),它会修改将要消失的对象,因此无论如何都无法获得修改后的值。他说,这很可能是一个错误。
sbi

2
@sbk:很抱歉,如果我冒犯了您,但我不认为2)在挑剔。除非您这样设置,否则右值根本不是const,并且可以更改它们,除非它们是内置的。我花了一些时间来了解,例如在使用字符串示例时,我是否做错了什么(JFTR:我没有),所以我倾向于认真对待这一区别。
2009年

5
我同意sbi-这件事根本不是挑剔的。最好将类类型的右值保持为非常量,这是移动语义的基础。
Johannes Schaub-litb

38

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

为了防止用户意外修改将在表达式末尾消失的对象,这是一个有意的设计决策:

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

如果要执行此操作,则必须先制作该对象的本地副本或将其绑定到const引用:

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 ++标准将包含右值引用。因此,您所知道的引用称为“左值引用”。您将被允许将右值绑定到右值引用,并且可以在“ rvalue-ness”上重载函数:

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()
2014年

嗯,你是什么意思getx()(不是get(x)
SexyBeast

当我写“ ... getx()(而不是get(x))...”时,我的意思是该函数的名称是getx(),而不是get(x)(如您所写的)。
2014年

这个答案混淆了术语。右值是一个表达式类别。临时对象是一个对象。右值可以表示临时对象,也可以不表示。临时对象可以或可以不由右值表示。
MM

16

您显示的是允许操作员链接。

 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参考。您在样本中说“ Note x是变量的别名。您要更新什么变量。” 没问题。有一个唯一变量(临时变量)。必须更改某些临时对象(等于5)。
2009年

@马丁:晃来晃去的引用不仅不整洁。在该方法的后续版本中访问它们可能会导致严重的错误!
mmmmmmmm

1
@Alexey:请注意,将其绑定到const引用会延长临时对象的生存期,这是故意添加的例外(TTBOMK是为了允许手动优化)。没有为非常量引用添加异常,因为将临时绑定到非常量引用最有可能是程序员错误。
sbi

1
@alexy:对可见变量的引用!不那么直观。
马丁·约克

const_cast<x&>(getX());没有道理
curiousguy 2011年

10

为什么 C ++ FAQ(我的黑体字)中进行了讨论:

在C ++中,非const引用可以绑定到左值,而const引用可以绑定到左值或rvalue,但是没有什么可以绑定到非const rvalue。这是为了防止人们在使用新值之前更改被破坏的临时值。例如:

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。


看到被Fortran的“零错误”咬伤的程序员的面孔真是很有趣!x * 0 x?什么?什么??
约翰D

最后一个论点特别弱。值得一提的编译器实际上不会将0的值更改为1,甚至不会以incr(0);这种方式进行解释。显然,如果允许这样做,则将其解释为创建一个临时整数并将其传递给incr()
user3204459

6

主要的问题是

g(getx()); //error

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

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

之所以有效,是因为有时您需要重用表达式的结果,而且很明显,您正在处理一个临时对象。

但是不可能

X& x = getx(); // error

有效而不g(getx())有效,这是语言设计师首先要避免的。

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

之所以有效,是因为方法仅知道的恒定性,this而不知道是在左值还是右值上调用它们。

像在C ++中一样,您对此规则有一种解决方法,但是必须通过显式告知编译器您知道自己在做什么:

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

5

关于为什么不允许这样做的原始问题似乎已经被清楚地回答:“因为这很可能是错误”。

FWIW,尽管我认为这不是一个好方法,但我想我将展示如何做到这一点。

我有时想要将临时变量传递给采用非常量引用的方法的原因是,有意丢弃调用方法不关心的按引用返回的值。像这样:

// 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
}

- 克里斯


4

你为什么要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 2011年

依赖RVO ”,但不称为“ RVO”。
curiousguy 2011年

2
@curiousguy:这是一个非常被接受的术语。将其称为“ RVO”绝对没有错。
小狗


3

很好的问题,这是我尝试的一个更简洁的答案(因为很多有用的信息都在注释中,而很难从噪音中挖掘出来。)

直接绑定到临时目录的任何引用都将延长其寿命[12.2.5]。另一方面,用另一个引用初始化的引用将不会(即使最终最终是相同的临时)。那是有道理的(编译器不知道该引用最终指的是什么)。

但是,整个想法非常令人困惑。例如,const X &x = X();将使临时对象的持续时间与x引用的时间一样长,但const X &x = X().ref();不会(知道ref()实际返回的内容)的对象不会持续。在后一种情况下,析构函数for X在此行的末尾被调用。(这可以通过非平凡的析构函数观察到。)

因此,这似乎通常令人困惑和危险(为什么使有关对象生存期的规则变得复杂?),但是大概至少需要const引用,因此该标准确实为它们设置了这种行为。

[来自sbi注释]:请注意,将其绑定到const引用可以延长临时对象的生存期,这是特意添加的例外(为了允许手动优化而添加了TTBOMK)。没有为非常量引用添加异常,因为将临时绑定到非常量引用最有可能是程序员错误。

所有临时对象都将持续到整个表达式结束。但是,要使用它们,您需要使用的技巧ref()。那是合法的。似乎没有足够的理由跳过额外的循环,只是提醒程序员发生了异常情况(即,修改后的参考参数将很快丢失)。

[另一个sbi评论] Stroustrup给出了(在D&E中)不允许将rvalue绑定到非const引用的原因是,如果Alexey的g()会修改对象(您希望从使用非const的函数中得到该对象)参考),它将修改将要消失的对象,因此无论如何也没人能获得修改后的值。他说,这很可能是一个错误。


2

“很明显,上面的示例中临时对象不是常量,因为允许调用非常量函数。例如,ref()可以修改临时对象。”

在您的示例中,getX()不返回const X,因此您可以像调用X()。ref()一样调用ref()。您正在返回非const引用,因此可以调用非const方法,您不能做的就是将ref分配给非const引用。

连同SadSidos评论,这会使您的三点错误。


1

我有一个场景,我想分享我希望我能做一下Alexey所要求的事情。在Maya C ++插件中,我必须执行以下操作,以便将值添加到node属性中:

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所描述的。我欢迎所有批评和建议!

谢谢。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.