为什么临时使用右值的地址是违法的

Why is it illegal to take the address of an rvalue temporary?

本文关键字:地址 为什么      更新时间:2023-10-16

根据"如何绕过警告"用作左值的右值",Visual Studio只会对以下代码发出警告:

int bar() {
   return 3;
}
void foo(int* ptr) {
}
int main() {
   foo(&bar());
}

在C++中,不允许获取临时(或者,至少是右值表达式引用的对象的地址?)的地址,我认为这是因为临时甚至不能保证有存储空间。

但是,尽管诊断可以以编译器选择的任何形式呈现,但在这种情况下,我仍然希望MSVS出现错误,而不是警告

那么,临时仓库是否保证有仓库?如果是这样的话,为什么上述代码一开始就被禁止?

实际上,在最初的语言设计中,它被允许使用临时地址。正如您正确地注意到的,没有任何技术原因不允许这样做,MSVC今天仍然通过非标准的语言扩展允许这样做。

C++将其定为非法的原因是,对临时库的绑定引用与从C:隐式类型转换继承的另一个C++语言功能冲突。考虑:

void CalculateStuff(long& out_param) {
    long result;
    // [...] complicated calculations
    out_param = result;
}
int stuff;
CalculateStuff(stuff);  //< this won't compile in ISO C++

CCD_ 1应该通过输出参数返回其结果。但实际发生的情况是:函数接受long&,但被赋予类型为int的参数。通过C的隐式类型转换,int现在隐式转换为long类型的变量,在过程中创建了一个未命名的临时。因此,该函数实际上对一个未命名的临时对象进行操作,而不是变量stuff,一旦该临时对象被破坏,该函数应用的所有副作用都将丢失。变量stuff的值从未改变。

引入对C++的引用是为了允许运算符重载,因为从调用方的角度来看,它们在语法上与按值调用相同(与指针调用不同,指针调用需要在调用方一侧显式&)。不幸的是,当与C的隐式类型转换相结合时,正是这种语法等价导致了问题。

由于Stroustrup希望保留这两个特性(引用和C兼容性),他引入了我们今天都知道的规则:未命名的临时库只绑定到const引用。有了这个附加规则,上面的示例就不再编译了。由于只有当函数将副作用应用于引用参数时才会出现问题,因此将未命名的临时变量绑定到常量引用仍然是安全的,因此这仍然是允许的。

整个故事也在C++的设计和进化:的第3.7章中进行了描述

允许引用由非lvalues初始化的原因是允许按值调用和按引用调用之间的区别是由调用函数指定的细节,调用方对此不感兴趣。对于const参考文献,这是可能的;对于CalculateStuff()0引用,它不是。对于Release 2.0,C++的定义被更改以反映这一点。

我还隐约记得在一篇论文中读到谁第一次发现了这种行为,但我现在记不清了。也许有人能帮我?

你说的"临时存储器甚至不能保证有存储空间"是对的,因为临时存储器可能不会存储在可寻址存储器中。事实上,为RISC体系结构(如ARM)编译的函数通常会在通用寄存器中返回值,并期望在这些寄存器中也有输入。

为x86体系结构生成代码的MSVS可能总是生成在堆栈上返回其值的函数。因此,它们存储在可寻址存储器中,并且具有有效地址。

当然临时仓库有存储空间。你可以这样做:

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中,您也可以使用非常量右值引用来实现这一点:

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++标准仅调用诊断(即错误警告),编译器可能会拒绝程序,或者编译程序,在运行时具有未定义的行为。如果您希望编译器严格拒绝生成诊断的程序,请将其配置为将警告转换为错误。

临时对象确实有内存。有时编译器也会创建临时文件。在坑洞的情况下,这些物体即将消失,也就是说,它们不应该偶然收集到重要的变化。因此,只能通过右值引用或常量引用获取临时值,而不能通过非常量引用获取。获取一个即将消失的对象的地址也感觉像是一件危险的事情,因此不受支持。

如果你确定你真的想要一个来自临时对象的非常数引用或指针,你可以从相应的成员函数返回它:你可以在临时对象上调用非常数成员函数。您可以从该成员处返回this。但是,请注意,类型系统正试图帮助您。当你欺骗它时,你最好知道你所做的是正确的。

正如其他人所提到的,我们都同意临时仓库确实有存储空间。

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

因为临时地址是在堆栈上分配的,所以编译器可以自由地将该地址用于任何其他目的

int foo()
{
int myvar=5;
return &myvar;
}
int main()
{
int *p=foo();
print("%d", *p);
return 0;
}

假设"myvar"的地址为0x1000。这个程序很可能会打印99,即使在main()中访问0x1000是非法的。不过,不一定总是这样。

对上面的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",还是在其中任何一个printfs中打印,都是对堆栈内存使用/分配方式的无意副作用。这就是为什么访问作用域中不存在活动的地址是非法的。

临时仓库确实有存储空间。它们被分配在调用者的堆栈上(注意:可能是调用约定的主题,但我认为它们都使用调用者的堆栈):

caller()
{
 callee1( Tmp() );
 callee2( Tmp() );
}

编译器将在caller的堆栈上为结果Tmp()分配空间。您可以获取这个内存位置的地址——它将是caller堆栈上的某个地址。编译器不能保证的是,在callee返回后,它将保留该堆栈地址的值。例如,编译器可以在那里放置另一个临时等

编辑:我相信,不允许删除这样的代码:

T bar();
T * ptr = &bar();

因为这很可能会导致问题。

编辑:这里有一个小测试:

#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 ) = lAtttt**" << 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)tt**" << 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)tt**" << 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的堆栈上保留了临时隐藏的局部变量,并且MIGHT静默地重用它们。一切正常,直到最后一次手动超控:在最后一次人工超控之后,我们有额外的单独调用std::cout。它使用堆栈空间到我们刚写东西的地方,结果我们得到了垃圾。

一句话:GCC和VC++都为调用方堆栈上的临时内存分配空间。他们可能有不同的策略来分配多少空间,如何重用这个空间(这也可能取决于优化)。它们都可能自行重用这个空间,因此,获取临时地址是不安全的,因为我们可能会尝试通过这个地址访问我们认为它仍然存在的值(比如,直接在那里写一些东西,然后尝试检索它),而编译器可能已经重用了它,并覆盖了我们的值。