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

How come a non-const reference cannot bind to a temporary object?

提问人:Alexey Malistov 提问时间:10/14/2009 最后编辑:curiousguyAlexey Malistov 更新时间:9/10/2023 访问量:110769

问:

为什么不允许获取对临时对象的非常量引用, 哪个函数返回?显然,这是C++标准所禁止的 但我对这种限制的目的感兴趣,而不是对标准的引用getx()

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

另外:

他们说“将一个临时对象分配给常量引用会延长这个对象的生存期”和“不过,没有关于非常量引用的说法”。 我的另一个问题。以下赋值是否会延长临时对象的生存期?

X& x = getx().ref(); // OK
C++ 引用 常量 临时 C++-FAQ

评论

4赞 SadSido 10/14/2009
我不同意“对象的生存期不能成为原因”部分,只是因为标准中指出,将临时对象分配给常量引用会将该对象的生存期延长到常量引用的生存期。不过,没有提到非常量引用......
3赞 Alexey Malistov 10/14/2009
好吧,是什么原因造成的“虽然没有关于非常量引用......”。这是我问题的一部分。这有什么意义吗?可能是标准的作者只是忘记了非常量引用,很快我们就会看到下一个核心问题?
3赞 Fernando N. 10/14/2009
GotW #88:“最重要的常量”的候选者。herbsutter.spaces.live.com/blog/cns!2D4327CC297151BB!378.entry
5赞 sbi 10/15/2009
@Michael:VC 将右值绑定到非常量引用。他们称之为功能,但实际上这是一个错误。(请注意,这不是一个错误,因为它本质上是不合逻辑的,而是因为它被明确排除以防止愚蠢的错误。
7赞 sarnold 4/23/2012
赫伯·萨特(Herb Sutter)的GotW #88:“最重要常量”的候选人已经移动。

答:

3赞 DevFred 10/14/2009 #1

你为什么要?只需使用并依赖 RVO。X& x = getx();X x = getx();

评论

3赞 Alexey Malistov 10/14/2009
Becouse我想打电话而不是g(getx())g(getx().ref())
4赞 Johannes Schaub - litb 10/15/2009
@Alexey,这不是一个真正的原因。如果你这样做,那么你就会有一个逻辑错误,因为要修改一些你无法再得到的东西。g
3赞 curiousguy 12/7/2011
@JohannesSchaub litb,也许他不在乎。
0赞 curiousguy 12/7/2011
"依赖 RVO“,但它不被称为”RVO”。
2赞 Puppy 12/7/2011
@curiousguy:这是一个非常被接受的术语。将其称为“RVO”绝对没有错。
1赞 Patrick 10/14/2009 #2

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

在您的示例中,getX() 不返回 const X,因此您可以以与调用 X().ref() 大致相同的方式调用 ref()。您返回的是非 const ref,因此可以调用 non const 方法,您不能做的是将 ref 分配给非常量引用。

与SadSidos的评论一起,这使你的三点不正确。

113赞 sbk 10/14/2009 #3

摘自这篇关于右值引用的 Visual C++ 博客文章

...C++不希望你意外 修改临时,但直接 调用非常量成员函数 可修改的右值是显式的,因此 这是允许的......

基本上,你不应该尝试修改临时对象,因为它们是临时对象,现在随时都会死去。允许你调用非常量方法的原因是,只要你知道你在做什么,并且你明确地使用它(比如,使用reinterpret_cast),欢迎你做一些“愚蠢”的事情。但是,如果你将一个临时引用绑定到一个非常量引用,你可以“永远”地传递它,只是为了让你对对象的操作消失,因为在某个地方,你完全忘记了这是一个暂时的。

如果我是你,我会重新考虑我的功能设计。为什么 g() 接受引用,它会修改参数吗?如果不是,请将其作为常量引用,如果是,您为什么要尝试将临时传递给它,您不在乎您正在修改它是临时的吗?为什么 getx() 返回是临时的?如果您与我们分享您的真实场景以及您想要完成的任务,您可能会得到一些关于如何做到这一点的好建议。

违背语言并愚弄编译器很少能解决问题——通常它会产生问题。


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

  2. 我清楚地说明了目的:不允许修改临时,因为它没有意义(忽略 C++0x 右值引用)。“那为什么允许我打电话给非常量成员?”这个问题很好,但我没有比我上面已经说过的更好的答案了。

  3. 好吧,如果我在陈述末尾的死亡中对 x 的看法是正确的,那么问题就很明显了。X& x = getx().ref();

无论如何,根据您的问题和评论,我认为即使是这些额外的答案也不会让您满意。这是最后的尝试/总结:C++ 委员会认为修改临时引用没有意义,因此,他们不允许绑定到非常量引用。可能还涉及一些编译器实现或历史问题,我不知道。然后,出现了一些具体的情况,并决定不顾一切,他们仍然允许通过调用非常量方法进行直接修改。但这是一个例外 - 您通常不允许修改临时。是的,C++ 通常很奇怪。

评论

2赞 sbi 10/15/2009
@sbk:1)实际上,正确的短语是:“......在完整表达式的末尾......”。我认为,“完整表达”被定义为不是其他表达的子表达。这是否总是与“声明的结尾”相同,我不确定。
5赞 sbi 10/15/2009
@sbk: 2) 实际上,您可以修改右值(临时值)。对于内置类型(等)是禁止的,但对于用户定义的类型是允许的:。int(std::string("A")+"B").append("C")
7赞 sbi 10/15/2009
@sbk:3)斯特劳斯特鲁普(在D&E中)不允许将右值绑定到非常量引用的原因是,如果阿列克谢修改对象(你期望函数采用非常量引用),它将修改一个将要死亡的对象,所以无论如何都没有人能得到修改后的值。他说,这很可能是一个错误。g()
2赞 sbi 10/15/2009
@sbk:如果我冒犯了你,我很抱歉,但我不认为 2) 是吹毛求疵。Rvalues 根本不是恒定的,除非你让它们如此,你可以改变它们,除非它们是内置的。我花了一段时间才明白,例如,字符串示例,我是否做错了什么(JFTR:我没有),所以我倾向于认真对待这种区别。
5赞 Johannes Schaub - litb 10/15/2009
我同意 sbi 的观点——这件事根本不是吹毛求疵。移动语义的所有基础是类类型右值最好保持非常量。
47赞 sbi 10/14/2009 #4

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

这是一个经过深思熟虑的设计决策,以防止用户意外修改在表达式末尾将要死亡的对象:

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++ 标准将包括右值引用。因此,您所知道的参考文献被称为“左值参考文献”。您将被允许将右值绑定到右值引用,并且可以在“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赞 NedStarkOfWinterfell 11/8/2014
嗨,这是一个很棒的答案。需要知道一件事,它不起作用,因为它的签名是并返回一个临时对象,所以我们不能将临时对象(rvalue)绑定到非常量引用,对吗?在你的第一个代码片段中,我认为它将是 而不是 ..g(getx())g(X& x)get(x)const X& x2 = getx();const X& x1 = getx();
1赞 sbi 11/9/2014
感谢您在我写完 5 年后在我的回答中指出这个错误! 是的,你的推理是正确的,尽管有点倒退:我们不能将临时引用绑定到非(左值)引用,因此 (和 not ) 返回的临时引用不能绑定到作为参数的左值引用。:-/constgetx()get(x)g()
0赞 NedStarkOfWinterfell 11/9/2014
嗯,你说的 getx() (而不是 get(x)) 是什么意思?
0赞 sbi 11/9/2014
当我写“......getx() (而不是 get(x))...“,我的意思是函数的名称是 ,而不是(正如你所写的)。getx()get(x)
0赞 M.M 5/8/2017
这个答案混淆了术语。右值是一个表达式类别。临时对象就是一个对象。右值可能表示也可能不表示临时对象;临时对象可能用右值表示,也可能不用右值表示。
19赞 Martin York 10/14/2009 #5

您显示的是允许运算符链接。

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

表达式是 'getx().ref();',在赋值到 'x' 之前执行完成。

请注意,getx() 不会返回引用,而是返回一个完全形成的对象到本地上下文中。该对象是临时的,但不是常量,因此允许您调用其他方法来计算值或发生其他副作用。

// 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());

使用对临时对象的常量引用可以执行此操作的原因是,该标准将临时对象的生存期延长到引用的生存期,因此临时对象的生存期将延长到语句末尾之后。

因此,剩下的唯一问题是,为什么标准不想允许引用临时语句以将对象的生存期延长到语句末尾之后?

我相信这是因为这样做会使编译器很难获得临时对象的正确性。它是为对临时函数的常量引用而完成的,因为这使用有限,因此迫使您复制对象以执行任何有用的操作,但确实提供了一些有限的功能。

想想这种情况:

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赞 Alexey Malistov 10/14/2009
“因此,唯一剩下的问题是,为什么标准不想允许引用临时词,以将对象的寿命延长到语句末尾之后?”就是这样!明白我的问题。但我不同意你的意见。你说“使编译器非常困难”,但它是为了常量参考而做的。您在示例中说“注意 x 是变量的别名。你要更新什么变量。没关系。存在唯一变量(临时变量)。必须更改某些临时对象(等于 5)。
0赞 mmmmmmmm 10/14/2009
@Martin:悬空的引用不仅不整洁。当稍后在方法中访问时,它们可能会导致严重的错误!
1赞 sbi 10/15/2009
@Alexey:请注意,将其绑定到 const 引用可以增强临时引用的生存期这一事实是故意添加的例外(TTBOMK 以允许手动优化)。没有为非常量引用添加异常,因为将临时引用绑定到非常量引用很可能是程序员错误。
1赞 Martin York 10/15/2009
@alexy:对不可见变量的引用!没那么直观。
0赞 curiousguy 12/7/2011
const_cast<x&>(getX());没有意义
3赞 paul 10/14/2009 #6

邪恶的解决方法涉及“可变”关键字。实际上,邪恶是留给读者的练习。或看这里:http://www.ddj.com/cpp/184403758

6赞 Dan Berindei 11/13/2009 #7

主要问题是

g(getx()); //error

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

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

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

但是,不可能使

X& x = getx(); // error

有效而不使有效,这是语言设计者首先试图避免的。g(getx())

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

是有效的,因为方法只知道 的常量,它们不知道它们是在左值还是右值上调用的。this

与 C++ 中一样,您有此规则的解决方法,但您必须通过显式向编译器发出信号,表明您知道自己在做什么:

g(const_cast<x&>(getX()));
4赞 DS. 4/9/2012 #8

很好的问题,这是我试图更简洁的答案(因为很多有用的信息都在评论中,很难在噪音中挖掘出来。

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

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

因此,它通常看起来令人困惑和危险(为什么要使有关对象生存期的规则复杂化?),但据推测,至少需要常量引用,因此标准确实为它们设置了这种行为。

[来自 sbi 注释]:请注意,将其绑定到 const 引用会增强 Temporary 的 Lifetime 是特意添加的例外 (TTBOMK 以便允许手动优化)。没有一个 为非常量引用添加了异常,因为绑定了临时引用 对非常量引用被视为最有可能是程序员 错误。

所有临时表达式都会持续到完整表达式结束。但是,要利用它们,您需要像 .这是合法的。似乎没有充分的理由让额外的箍跳过,除了提醒程序员正在发生一些不寻常的事情(即,一个修改将很快丢失的参考参数)。ref()

[另一个sbi评论]Stroustrup给出的原因(在D&E中)不允许绑定 非常量引用的 rvalues 是,如果 Alexey 的 g() 将修改 对象(你期望从采用非 const 的函数中得到的对象 reference),它会修改一个将要死亡的对象,所以没有人 无论如何都可以得到修改后的值。他说,这个,大多数 很可能是一个错误。

6赞 Chris Pearson 2/22/2013 #9

似乎关于为什么不允许这样做的原始问题已经得到了明确的回答:“因为它很可能是一个错误”。

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
}

--克里斯

评论

0赞 c z 4/20/2023
很好的例子。这工作的事实表明,错误不是由于 C++ 中的逻辑限制而产生的,而是由于设计决策而产生的。
1赞 Robert Trussardi 6/23/2013 #10

我有一个场景想分享,我希望我能按照阿列克谢的要求去做。在 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 被复制得那么多。我只需要临时对象一直存在到第二个函数结束。

嗯,这是我的场景。只是想我会举一个例子,人们想做阿列克谢所描述的事情。我欢迎所有批评和建议!

谢谢。

16赞 Tony Delroy 12/30/2015 #11

为什么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赞 John D 5/15/2016
看到被 Fortran “零错误”咬伤的程序员的脸会很有趣! 给?什么?什么??x * 0 x
2赞 user3204459 3/22/2017
最后一个论点特别薄弱。没有一个值得一提的编译器会真正将 0 的值更改为 1,甚至以这种方式进行解释。显然,如果允许这样做,它将被解释为创建一个临时整数并将其传递给incr(0);incr()
1赞 Jack O'Connor 8/24/2021
这是正确答案。当涉及隐式转换时,这种丢弃的副作用问题会变得更糟。例如,假设您更改为 .现在,表达式正在转换为临时表达式并通过引用传递它。现在内部的修改对调用方没有影响。这将是非常令人困惑的。Howard Hinnant 的原始移动语义提案中讨论了这个问题:open-std.org/jtc1/sc22/wg21/docs/papers/2002/...incr(int& a)incr(long& a)incr(i)ilongincr
0赞 Jack O'Connor 8/24/2021
const 引用避免了这个特定问题,因为如果没有恶作剧,就无法通过 const 引用进行编写。但它们仍可能导致其他相关问题。例如,如果返回给定的 const 引用,则返回引用的有效生存期取决于参数是左值(在这种情况下,它在某些范围内有效)还是临时右值(在这种情况下,它在语句末尾失效)。这可以像上面的情况一样悄无声息地改变。