根据“How to get around the warning "rvalue used as lvalue"?”,Visual Studio只会警告代码如下:
int bar() {
return 3;
}
void foo(int* ptr) {
}
int main() {
foo(&bar());
}
在C ++中,不允许获取临时(或者至少是 rvalue 表达式引用的对象?)的地址,而我我认为这是因为临时保证甚至不能存储。
但是,虽然诊断程序可能以编译器选择的任何形式呈现,但在这种情况下,我仍然期望MSVS 错误而不是警告。 / p>
那么,临时保证有存储空间吗?如果是这样,为什么上面的代码首先被禁止?
答案 0 :(得分:43)
实际上,在原始语言设计中, 允许使用临时的地址。正如您已经注意到的那样,没有技术原因不允许这样做,MSVC今天仍然允许通过非标准语言扩展。
C ++使其成为非法的原因是对temporaries的绑定引用与从C:Implicit类型转换继承的另一个C ++语言特性冲突。 考虑:
void CalculateStuff(long& out_param) {
long result;
// [...] complicated calculations
out_param = result;
}
int stuff;
CalculateStuff(stuff); //< this won't compile in ISO C++
CalculateStuff()
应该通过输出参数返回其结果。但真正发生的是:函数接受long&
但是给出了int
类型的参数。通过C的隐式类型转换,int
现在隐式转换为long
类型的变量,在流程中创建一个未命名的临时。
因此,该函数实际上对未命名的临时函数进行操作,而不是变量stuff
,并且一旦临时销毁,该函数应用的所有副作用都将丢失。变量stuff
的值永远不会改变。
引入了C ++的引用以允许运算符重载,因为从调用者的角度来看,它们在语法上与按值调用相同(而不是指针调用,它需要显式的&
在来电者的一边)。不幸的是,当与C&C的隐式类型转换相结合时,语法等价会导致麻烦。
由于Stroustrup希望保留这两个特性(引用和C兼容性),他引入了我们今天都知道的规则:未命名的临时代表只绑定到const引用。使用该附加规则,上述示例不再编译。由于问题仅在函数将副作用应用于引用参数时才会发生,因此将未命名的临时值绑定到const引用仍然是安全的,因此仍然允许这样做。
这整个故事也在C ++的设计和演变的第3.7章中描述:
允许通过非左值初始化引用的原因是允许将call-by-value和call-by-reference区分为被调用函数指定的细节,并且对调用者不感兴趣。对于
const
引用,这是可能的;对于non-const
引用它不是。对于版本2.0,更改了C ++的定义以反映这一点。
我还清醒地记得在一篇首次发现这种行为的论文中读书,但我现在还记不住了。也许有人可以帮助我?
答案 1 :(得分:10)
当然临时拥有存储空间。你可以这样做:
template<typename T>
const T *get_temporary_address(const T &x) {
return &x;
}
int bar() { return 42; }
int main() {
std::cout << (const void *)get_temporary_address(bar()) << std::endl;
}
在C ++ 11中,您也可以使用非const右值引用执行此操作:
template<typename T>
T *get_temporary_address(T &&x) {
return &x;
}
int bar() { return 42; }
int main() {
std::cout << (const void *)get_temporary_address(bar()) << std::endl;
}
注意,当然,取消引用有问题的指针(get_temporary_address
本身之外)是一个非常糟糕的主意;临时只存在于完整表达式的末尾,所以有一个指向它的指针逃脱表达式几乎总是一个灾难的处方。
此外,请注意,拒绝无效程序不需要编译器。 C和C ++标准只需要诊断(即错误或警告),编译器可能拒绝该程序,或可能 >编译程序,在运行时具有未定义的行为。如果您希望编译器严格拒绝产生诊断的程序,请将其配置为将警告转换为错误。
答案 2 :(得分:7)
你说“暂时不能保证存储”是正确的,因为暂时可能不会存储在可寻址的内存中。事实上,为RISC架构(例如ARM)编译的函数通常会返回通用寄存器中的值,并且也会期望这些寄存器中的输入。
MSVS,为x86体系结构生成代码,可能总是生成在堆栈上返回其值的函数。因此,它们存储在可寻址存储器中并具有有效地址。
答案 3 :(得分:4)
临时对象确实有内存。有时编译器也会创建临时对象。在这些情况下,这些物体即将消失,即它们不应该偶然地收集重要的变化。因此,您只能通过右值引用或const引用来获取临时值,但不能通过非const引用来获取临时值。取一个即将消失的物体的地址也感觉像是危险的东西,因此不受支持。
如果您确定您确实需要非const引用或来自临时对象的指针,则可以从相应的成员函数返回它:您可以在临时对象上调用非const成员函数。您可以从此会员返回this
。但请注意,类型系统正试图帮助您。当你欺骗它时,你最好知道你正在做什么是正确的事情。
答案 4 :(得分:3)
正如其他人所说,我们都同意临时工作确实存储。
为什么拿一个临时的地址是违法的?
因为temporaries是在堆栈上分配的,所以编译器可以自由地将该地址用于它想要的任何其他目的。
int foo()
{
int myvar=5;
return &myvar;
}
int main()
{
int *p=foo();
print("%d", *p);
return 0;
}
假设'myvar'的地址是0x1000。即使在main()中访问0x1000是非法的,该程序也很可能会打印99。虽然,不一定总是。
稍微更改上面的main():
int foo()
{
int myvar=5;
return &myvar; // address of myvar is 0x1000
}
int main()
{
int *p=foo(); //illegal to access 0x1000 here
print("%d", *p);
fun(p); // passing *that address* to fun()
return 0;
}
void fun(int *q)
{
int a,b; //some variables
print("%d", *q);
}
第二个printf不太可能打印'5',因为编译器甚至可以为fun()分配相同的堆栈部分(包含0x1000)。无论是对于两个printfs还是在其中任何一个中打印'5',它都是纯粹对堆栈内存的使用/分配方式的无意的副作用。这就是为什么访问范围内 alive 的地址是非法的。
答案 5 :(得分:1)
Temporaries确实有存储空间。它们被分配在调用者的堆栈上(注意:可能是调用约定的主题,但我认为它们都使用调用者的堆栈):
caller()
{
callee1( Tmp() );
callee2( Tmp() );
}
编译器将在Tmp()
的堆栈上为结果caller
分配空间。您可以获取此内存位置的地址 - 它将是caller
堆栈上的某个地址。编译器不保证的是它会在callee
返回后保留此堆栈地址的值。例如,编译器可以在那里放置另一个临时等。
编辑:我相信,不允许删除这样的代码:
T bar();
T * ptr = &bar();
因为它很可能会导致问题。
编辑:这是little test:
#include <iostream>
typedef long long int T64;
T64 ** foo( T64 * fA )
{
std::cout << "Address of tmp inside callee : " << &fA << std::endl;
return ( &fA );
}
int main( void )
{
T64 lA = -1;
T64 lB = -2;
T64 lC = -3;
T64 lD = -4;
T64 ** ptr_tmp = foo( &lA );
std::cout << "**ptr_tmp = *(*ptr_tmp ) = lA\t\t\t\t**" << ptr_tmp << " = *(" << *ptr_tmp << ") = " << **ptr_tmp << " = " << lA << std::endl << std::endl;
foo( &lB );
std::cout << "**ptr_tmp = *(*ptr_tmp ) = lB (compiler override)\t**" << ptr_tmp << " = *(" << *ptr_tmp << ") = " << **ptr_tmp << " = " << lB << std::endl
<< std::endl;
*ptr_tmp = &lC;
std::cout << "Manual override" << std::endl << "**ptr_tmp = *(*ptr_tmp ) = lC (manual override)\t\t**" << ptr_tmp << " = *(" << *ptr_tmp << ") = " << **ptr_tmp
<< " = " << lC << std::endl << std::endl;
*ptr_tmp = &lD;
std::cout << "Another attempt to manually override" << std::endl;
std::cout << "**ptr_tmp = *(*ptr_tmp ) = lD (manual override)\t\t**" << ptr_tmp << " = *(" << *ptr_tmp << ") = " << **ptr_tmp << " = " << lD << std::endl
<< std::endl;
return ( 0 );
}
程序输出GCC:
Address of tmp inside callee : 0xbfe172f0
**ptr_tmp = *(*ptr_tmp ) = lA **0xbfe172f0 = *(0xbfe17328) = -1 = -1
Address of tmp inside callee : 0xbfe172f0
**ptr_tmp = *(*ptr_tmp ) = lB (compiler override) **0xbfe172f0 = *(0xbfe17320) = -2 = -2
Manual override
**ptr_tmp = *(*ptr_tmp ) = lC (manual override) **0xbfe172f0 = *(0xbfe17318) = -3 = -3
Another attempt to manually override
**ptr_tmp = *(*ptr_tmp ) = lD (manual override) **0xbfe172f0 = *(0x804a3a0) = -5221865215862754004 = -4
程序输出VC ++:
Address of tmp inside callee : 00000000001EFC10
**ptr_tmp = *(*ptr_tmp ) = lA **00000000001EFC10 = *(000000013F42CB10) = -1 = -1
Address of tmp inside callee : 00000000001EFC10
**ptr_tmp = *(*ptr_tmp ) = lB (compiler override) **00000000001EFC10 = *(000000013F42CB10) = -2 = -2
Manual override
**ptr_tmp = *(*ptr_tmp ) = lC (manual override) **00000000001EFC10 = *(000000013F42CB10) = -3 = -3
Another attempt to manually override
**ptr_tmp = *(*ptr_tmp ) = lD (manual override) **00000000001EFC10 = *(000000013F42CB10) = 5356268064 = -4
请注意,GCC和VC ++都保留在main
隐藏的本地变量的堆栈上,并且可以静默地重用它们。一切正常,直到最后一次手动覆盖:在最后一次手动覆盖之后,我们对std::cout
进行了额外的单独调用。它使用堆栈空间到我们刚写的东西,结果我们得到了垃圾。
底线:GCC和VC ++都为调用者堆栈上的临时值分配空间。他们可能有不同的策略来分配多少空间,如何重用这个空间(它也可能依赖于优化)。他们都可以根据自己的判断重复使用这个空间,因此,获取临时地址是不安全的,因为我们可能会尝试通过这个地址访问我们认为它仍然具有的值(比如,直接在那里写一些东西然后尝试检索它,而编译器可能已经重用它并覆盖了我们的值。