提问人:Alexey Malistov 提问时间:10/14/2009 最后编辑:curiousguyAlexey Malistov 更新时间:9/10/2023 访问量:110769
为什么非常量引用不能绑定到临时对象?
How come a non-const reference cannot bind to a temporary object?
问:
为什么不允许获取对临时对象的非常量引用,
哪个函数返回?显然,这是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;
}
- 很明显,对象的生存期不能成为原因,因为 C++ 标准不禁止对对象的常量引用。
- 很明显,在上面的示例中,临时对象不是常量对象,因为允许调用非常量函数。例如,可以修改临时对象。
ref()
- 此外,允许您欺骗编译器并获取指向此临时对象的链接,从而解决了我们的问题。
ref()
另外:
他们说“将一个临时对象分配给常量引用会延长这个对象的生存期”和“不过,没有关于非常量引用的说法”。 我的另一个问题。以下赋值是否会延长临时对象的生存期?
X& x = getx().ref(); // OK
答:
你为什么要?只需使用并依赖 RVO。X& x = getx();
X x = getx();
评论
g(getx())
g(getx().ref())
g
“很明显,在上面的示例中,临时对象不是恒定的,因为调用 允许使用非常量函数。例如,ref() 可以修改临时 对象。
在您的示例中,getX() 不返回 const X,因此您可以以与调用 X().ref() 大致相同的方式调用 ref()。您返回的是非 const ref,因此可以调用 non const 方法,您不能做的是将 ref 分配给非常量引用。
与SadSidos的评论一起,这使你的三点不正确。
...C++不希望你意外 修改临时,但直接 调用非常量成员函数 可修改的右值是显式的,因此 这是允许的......
基本上,你不应该尝试修改临时对象,因为它们是临时对象,现在随时都会死去。允许你调用非常量方法的原因是,只要你知道你在做什么,并且你明确地使用它(比如,使用reinterpret_cast),欢迎你做一些“愚蠢”的事情。但是,如果你将一个临时引用绑定到一个非常量引用,你可以“永远”地传递它,只是为了让你对对象的操作消失,因为在某个地方,你完全忘记了这是一个暂时的。
如果我是你,我会重新考虑我的功能设计。为什么 g() 接受引用,它会修改参数吗?如果不是,请将其作为常量引用,如果是,您为什么要尝试将临时传递给它,您不在乎您正在修改它是临时的吗?为什么 getx() 返回是临时的?如果您与我们分享您的真实场景以及您想要完成的任务,您可能会得到一些关于如何做到这一点的好建议。
违背语言并愚弄编译器很少能解决问题——通常它会产生问题。
编辑:解决评论中的问题:
X& x = getx().ref(); // OK when will x die?
- 我不知道,我也不在乎,因为这正是我所说的“违背语言”的意思。该语言说“临时者在陈述的末尾死亡,除非他们有义务进行引用,在这种情况下,当引用超出范围时,他们就会死亡”。应用该规则,似乎 x 在下一个语句的开头已经死了,因为它没有绑定到 const 引用(编译器不知道 ref() 返回什么)。然而,这只是一个猜测。我清楚地说明了目的:不允许修改临时,因为它没有意义(忽略 C++0x 右值引用)。“那为什么允许我打电话给非常量成员?”这个问题很好,但我没有比我上面已经说过的更好的答案了。
好吧,如果我在陈述末尾的死亡中对 x 的看法是正确的,那么问题就很明显了。
X& x = getx().ref();
无论如何,根据您的问题和评论,我认为即使是这些额外的答案也不会让您满意。这是最后的尝试/总结:C++ 委员会认为修改临时引用没有意义,因此,他们不允许绑定到非常量引用。可能还涉及一些编译器实现或历史问题,我不知道。然后,出现了一些具体的情况,并决定不顾一切,他们仍然允许通过调用非常量方法进行直接修改。但这是一个例外 - 您通常不允许修改临时。是的,C++ 通常很奇怪。
评论
int
(std::string("A")+"B").append("C")
g()
在代码中返回一个临时对象,即所谓的“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
评论
g(getx())
g(X& x)
get(x)
const X& x2 = getx();
const X& x1 = getx();
:-/
const
getx()
get(x)
g()
的 getx()
(而不是 get(x))
是什么意思?
getx()
get(x)
您显示的是允许运算符链接。
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();
将为您提供直观易用和易于理解的代码。
如果要修改该值,则应将该值返回给变量。如果您试图避免从函数中复制目标的成本(因为该对象似乎是复制构造回来的(从技术上讲是这样))。那就别打扰编译器很擅长'返回值优化'了
评论
const_cast<x&>(getX());
没有意义
邪恶的解决方法涉及“可变”关键字。实际上,邪恶是留给读者的练习。或看这里:http://www.ddj.com/cpp/184403758
主要问题是
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()));
很好的问题,这是我试图更简洁的答案(因为很多有用的信息都在评论中,很难在噪音中挖掘出来。
任何直接绑定到临时引用的引用都将延长其生存期 [12.2.5]。另一方面,使用另一个引用初始化的引用不会(即使它最终是相同的临时引用)。这是有道理的(编译器不知道该引用最终指的是什么)。
但这整个想法非常令人困惑。例如 将使临时的持续时间与引用一样长,但不会(谁知道实际返回了什么)。在后一种情况下,析构函数 的调用位于此行的末尾。(这可以通过非平凡的析构函数观察到。const X &x = X();
x
const X &x = X().ref();
ref()
X
因此,它通常看起来令人困惑和危险(为什么要使有关对象生存期的规则复杂化?),但据推测,至少需要常量引用,因此标准确实为它们设置了这种行为。
[来自 sbi 注释]:请注意,将其绑定到 const 引用会增强 Temporary 的 Lifetime 是特意添加的例外 (TTBOMK 以便允许手动优化)。没有一个 为非常量引用添加了异常,因为绑定了临时引用 对非常量引用被视为最有可能是程序员 错误。
所有临时表达式都会持续到完整表达式结束。但是,要利用它们,您需要像 .这是合法的。似乎没有充分的理由让额外的箍跳过,除了提醒程序员正在发生一些不寻常的事情(即,一个修改将很快丢失的参考参数)。ref()
[另一个sbi评论]Stroustrup给出的原因(在D&E中)不允许绑定 非常量引用的 rvalues 是,如果 Alexey 的 g() 将修改 对象(你期望从采用非 const 的函数中得到的对象 reference),它会修改一个将要死亡的对象,所以没有人 无论如何都可以得到修改后的值。他说,这个,大多数 很可能是一个错误。
似乎关于为什么不允许这样做的原始问题已经得到了明确的回答:“因为它很可能是一个错误”。
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
}
--克里斯
评论
我有一个场景想分享,我希望我能按照阿列克谢的要求去做。在 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 被复制得那么多。我只需要临时对象一直存在到第二个函数结束。
嗯,这是我的场景。只是想我会举一个例子,人们想做阿列克谢所描述的事情。我欢迎所有批评和建议!
谢谢。
为什么在 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。
评论
x * 0
x
incr(0);
incr()
incr(int& a)
incr(long& a)
incr(i)
i
long
incr
评论