为啥非常量引用不能绑定到临时对象?
Posted
技术标签:
【中文标题】为啥非常量引用不能绑定到临时对象?【英文标题】:How come a non-const reference cannot bind to a temporary object?为什么非常量引用不能绑定到临时对象? 【发布时间】:2022-01-01 08:10:31 【问题描述】:为什么不允许获取对临时对象的非常量引用,
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;
-
很明显,对象的生命周期不可能是原因,因为
C++ 标准不禁止对对象的常量引用。
很明显,上述示例中的临时对象不是常量,因为允许调用非常量函数。例如,
ref()
可以修改临时对象。
此外,ref()
允许您欺骗编译器并获得指向此临时对象的链接,从而解决了我们的问题。
另外:
他们说“将一个临时对象分配给 const 引用会延长该对象的生命周期”和“尽管如此,关于非 const 引用什么也没说”。 我的其他问题。以下赋值会延长临时对象的生命周期吗?
X& x = getx().ref(); // OK
【问题讨论】:
我不同意“对象的生命周期不能成为原因”部分,只是因为标准中规定,将临时对象分配给 const 引用会将此对象的生命周期延长到const 引用的生命周期。虽然没有提到非常量引用... 好吧,那是什么原因导致“虽然关于非常量引用什么都没说......”。这是我的问题的一部分。这有什么意义吗?可能是标准的作者忘记了非常量引用,很快我们就会看到下一个核心问题? GotW #88:“最重要的 const”的候选人。 herbsutter.spaces.live.com/blog/cns!2D4327CC297151BB!378.entry @Michael:VC 将右值绑定到非常量引用。他们称之为功能,但实际上它是一个错误。 (请注意,这不是一个错误,因为它本质上是不合逻辑的,而是因为它被明确排除以防止愚蠢的错误。) Herb Sutter's GotW #88 : A Candidate For the "Most Important const" has moved. 【参考方案1】:来自Visual C++ blog article about rvalue references:
... C++ 不希望你不小心 修改临时文件,但直接 调用非常量成员函数 可修改的右值是显式的,所以 是允许的……
基本上,您不应该尝试修改临时对象,因为它们是临时对象并且现在随时都会死掉。允许您调用非常量方法的原因是,欢迎您做一些“愚蠢”的事情,只要您知道自己在做什么并且明确说明(例如,使用 reinterpret_cast)。但是,如果您将临时对象绑定到非常量引用,则可以“永远”继续传递它,只是为了让您对对象的操作消失,因为在此过程中您完全忘记了这是一个临时对象。
如果我是你,我会重新考虑我的功能设计。为什么 g() 接受引用,它会修改参数吗?如果不是,请将其设为 const 引用,如果是,您为什么尝试将临时传递给它,您不在乎它是您正在修改的临时吗?为什么 getx() 还是临时返回?如果您与我们分享您的真实场景以及您想要完成的工作,您可能会得到一些关于如何做到这一点的好建议。
违背语言并愚弄编译器很少能解决问题——通常会产生问题。
编辑:解决评论中的问题: 1)
X& x = getx().ref(); // OK when will x die?
- 我不知道,我不在乎,因为这正是我所说的“违背语言”的意思。该语言说“临时对象在语句结束时死亡,除非它们被绑定到 const 引用,在这种情况下,当引用超出范围时它们会死亡”。应用该规则,似乎 x 在下一条语句的开头已经死了,因为它没有绑定到 const 引用(编译器不知道 ref() 返回什么)。然而,这只是一个猜测。
2) 我清楚地说明了目的:不允许修改临时对象,因为它没有意义(忽略 C++0x 右值引用)。问题“那么为什么我可以调用非常量成员?”是一个很好的答案,但我没有比我上面已经说过的更好的答案了。
3) 好吧,如果我在 X& x = getx().ref();
中的 x 在语句末尾死亡的说法是正确的,那么问题就很明显了。
无论如何,根据您的问题和 cmets,我认为即使是这些额外的答案也不会让您满意。这是最后的尝试/总结:C++ 委员会认为修改临时对象没有意义,因此,他们不允许绑定到非 const 引用。可能还涉及到一些编译器实现或历史问题,我不知道。然后,出现了一些具体的情况,决定无论如何,他们仍然允许通过调用非常量方法直接修改。但这是一个例外 - 通常不允许您修改临时文件。是的,C++ 通常就是这么奇怪。
【讨论】:
@sbk: 1) 实际上,正确的短语是:“...在完整表达式的末尾...”。我相信,“完整表达式”被定义为不是某个其他表达式的子表达式。这是否总是与“语句的结尾”相同,我不确定。 @sbk: 2) 实际上,您被允许修改右值(临时值)。禁止用于内置类型(int
等),但允许用于用户定义类型:(std::string("A")+"B").append("C")
。
@sbk: 3) Stroustrup 给出的(在 D&E 中)不允许将右值绑定到非 const 引用的原因是,如果 Alexey 的 g()
会修改对象(你期望从一个采用非常量引用的函数中),它会修改一个将要死的对象,所以无论如何没人能得到修改后的值。他说这很可能是一个错误。
@sbk:如果我冒犯了你,我很抱歉,但我不认为 2) 是吹毛求疵。右值根本不是 const ,除非您这样做并且可以更改它们,除非它们是内置的。我花了一段时间才明白,例如字符串示例,我是否做错了什么(JFTR:我没有),所以我倾向于认真对待这种区别。
我同意 sbi - 这件事根本不是吹毛求疵。类类型右值最好保持非 const 是移动语义的全部基础。【参考方案2】:
在您的代码中getx()
返回一个临时对象,即所谓的“右值”。您可以将右值复制到对象(也称为变量)中或将它们绑定到 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++ 标准将包括右值引用。因此,您所知道的引用被称为“左值引用”。您将被允许将右值绑定到右值引用,并且可以在“右值”上重载函数:
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
【讨论】:
嗨,这是一个很棒的答案。需要知道一件事,g(getx())
不起作用,因为它的签名是g(X& x)
,而get(x)
返回一个临时对象,所以我们不能将临时对象(rvalue)绑定到非- 恒定参考,对吗?在您的第一个代码片段中,我认为它将是 const X& x2 = getx();
而不是 const X& x1 = getx();
..
感谢您在我写完 5 年后在我的回答中指出这个错误! :-/
是的,你的推理是正确的,尽管有点倒退:我们不能将临时对象绑定到非const
(左值)引用,因此getx()
(而不是get(x)
)返回的临时对象不能绑定到作为 g()
的参数的左值引用。
嗯,你说的 getx()
(而不是 get(x)
)是什么意思?
当我写 "...getx() (而不是 get(x))..." 时,我的意思是函数的名字是getx()
,并且不是get(x)
(如你所写)。
这个答案混淆了术语。右值是一个表达式类别。临时对象是对象。右值可能表示也可能不表示临时对象;临时对象可能用右值表示,也可能不表示。【参考方案3】:
您所展示的是允许运算符链接。
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;
Look at the end of this answer for a better example of this
您可以不将临时对象绑定到引用,因为这样做会生成对对象的引用,该对象将在表达式末尾被销毁,从而留下一个悬空引用(这是不整洁的而且标准不喜欢凌乱)。
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();
将为您提供易于使用和理解的代码。
如果你想修改这个值,你应该把这个值返回给一个变量。如果您试图避免从函数复制对象的成本(因为似乎对象是复制构造回来的(从技术上讲是这样))。那就别打扰编译器很擅长'Return Value Optimization'
【讨论】:
“那么剩下的唯一问题是为什么标准不允许引用临时对象来延长对象的生命期超过语句的结尾?” 就是这样!你明白我的问题。但我不同意你的意见。您说“使编译器非常困难”,但它是为 const 参考而完成的。您在示例中说“注意 x 是变量的别名。您要更新什么变量。”没问题。有唯一变量(临时)。必须更改一些临时对象(等于 5)。 @Martin:悬空引用不仅不整洁。当稍后在方法中访问它们时,它们可能会导致严重的错误! @Alexey:请注意,将其绑定到 const 引用会增强临时对象的生命周期这一事实是一个例外,这是故意添加的(TTBOMK 是为了允许手动优化)。没有为非常量引用添加异常,因为将临时值绑定到非常量引用被认为很可能是程序员错误。 @alexy:对不可见变量的引用!没那么直观。const_cast<x&>(getX());
没有意义【参考方案4】:
为什么在C++ FAQ(粗体我的)中讨论:
在 C++ 中,非 const 引用可以绑定到左值,而 const 引用可以绑定到左值或右值,但没有什么可以绑定到非 const 右值。这是保护人们不改变在新价值可以使用之前被破坏的临时物品的价值。例如:
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
?什么?什么??
最后一个论点特别弱。任何值得一提的编译器都不会真正将 0 的值更改为 1,甚至不会以这种方式解释 incr(0);
。显然,如果允许这样做,它将被解释为创建一个临时整数并将其传递给incr()
这是正确答案。当涉及隐式转换时,这个副作用下降的问题会变得更糟。例如,假设您将 incr(int& a)
更改为 incr(long& a)
。现在表达式incr(i)
正在将i
转换为临时的long
并通过引用传递它。 incr
内部的修改现在对调用者没有影响。这将非常令人困惑。这个问题在 Howard Hinnant 的原始移动语义提案中讨论过:open-std.org/jtc1/sc22/wg21/docs/papers/2002/…
常量引用避免了这个特殊的问题,因为你不能在没有恶作剧的情况下写一个常量引用。但它们仍然可能导致其他相关问题。例如,如果您 return 给定的 const 引用,则返回的引用的有效生命周期取决于参数是左值(在这种情况下,它在某些范围内有效)还是临时右值(在这种情况下,它在语句结尾处死了)。这可以像上面的情况一样默默地改变。【参考方案5】:
主要问题是
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()));
【讨论】:
【参考方案6】:似乎关于为什么不允许这样做的原始问题已得到明确回答:“因为它很可能是一个错误”。
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
-- 克里斯
【讨论】:
【参考方案7】:为什么你会想要X& x = getx();
?只需使用 X x = getx();
并依赖 RVO。
【讨论】:
因为我想打电话给g(getx())
而不是g(getx().ref())
@Alexey,这不是真正的原因。如果你这样做了,那么你在某些地方就会出现逻辑错误,因为g
会修改一些你无法再动手的东西。
@JohannesSchaub-litb 也许他不在乎。
“依赖 RVO”,但它不称为“RVO”。
@curiousguy:这是一个非常接受的术语。将其称为“RVO”绝对没有错。【参考方案8】:
邪恶的解决方法涉及“可变”关键字。实际上是邪恶的留给读者作为练习。或者看这里:http://www.ddj.com/cpp/184403758
【讨论】:
【参考方案9】:很好的问题,这是我尝试更简洁的答案(因为很多有用的信息都在 cmets 中,很难在噪音中挖掘出来。)
任何直接绑定到临时对象的引用都会延长它的寿命 [12.2.5]。另一方面,用另一个引用初始化的引用将不会(即使它最终是相同的临时)。这是有道理的(编译器不知道该引用最终指的是什么)。
但这整个想法非常令人困惑。例如。 const X &x = X();
将使临时持续时间与 x
引用一样长,但 const X &x = X().ref();
不会(谁知道 ref()
实际返回了什么)。在后一种情况下,X
的析构函数在这一行的末尾被调用。 (这可以通过非平凡的析构函数观察到。)
因此,这似乎通常令人困惑和危险(为什么要使有关对象生命周期的规则复杂化?),但大概至少需要 const 引用,因此标准确实为它们设置了这种行为。
[来自sbi 评论]:请注意,将其绑定到 const 引用这一事实增强了 临时的生命周期是一个特意添加的例外 (TTBOMK 为了允许手动优化)。没有一个 为非常量引用添加了异常,因为绑定了一个临时的 非常量引用被认为很可能是程序员 错误。
所有临时对象都会持续到完整表达式结束。但是,要使用它们,您需要像 ref()
那样的技巧。那是合法的。似乎没有充分的理由让额外的箍跳过,除了提醒程序员正在发生一些不寻常的事情(即,一个参考参数,其修改将很快丢失)。
[另一个sbi 评论] Stroustrup 给出的原因(在 D&E 中)不允许绑定 对非常量引用的右值是,如果 Alexey 的 g() 会修改 对象(您期望从采用非常量的函数中获得 参考),它会修改一个会死的对象,所以没有人 无论如何都可以得到修改后的值。他说,大多数 很可能,是一个错误。
【讨论】:
【参考方案10】:"在上面的示例中,很明显临时对象不是常量,因为调用 非常量函数是允许的。例如, ref() 可以修改临时 对象。”
在您的示例中,getX() 不返回 const X,因此您可以像调用 X().ref() 一样调用 ref()。您正在返回一个非 const ref,因此可以调用非 const 方法,但不能将 ref 分配给非 const 引用。
加上 SadSidos 评论,这使您的三点不正确。
【讨论】:
【参考方案11】:我想分享一个场景,我希望我能按照 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++ 之外编译,因为它试图绑定从 | 返回的临时变量。运算符对
嗯,这是我的场景。只是想我会举一个例子,人们想做阿列克谢描述的事情。欢迎大家提出批评和建议!
谢谢。
【讨论】:
以上是关于为啥非常量引用不能绑定到临时对象?的主要内容,如果未能解决你的问题,请参考以下文章