为什么临时取rvalue的地址是违法的?

时间:2012-01-06 19:32:54

标签: c++

根据“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>

那么,临时保证有存储空间吗?如果是这样,为什么上面的代码首先被禁止?

6 个答案:

答案 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 ++都为调用者堆栈上的临时值分配空间。他们可能有不同的策略来分配多少空间,如何重用这个空间(它也可能依赖于优化)。他们都可以根据自己的判断重复使用这个空间,因此,获取临时地址是不安全的,因为我们可能会尝试通过这个地址访问我们认为它仍然具有的值(比如,直接在那里写一些东西然后尝试检索它,而编译器可能已经重用它并覆盖了我们的值。