我曾多次尝试这样编码:
struct Foo
{
double const& f;
Foo(double const& fx) : f(fx)
{
printf("%f %f\n", fx, this->f); // 125 125
}
double GetF() const
{
return f;
}
};
int main()
{
Foo p(123.0 + 2.0);
printf("%f\n", p.GetF()); // 0
return 0;
}
但它并没有崩溃。我还使用 valgrind 来测试程序,但没有出现错误或警告。所以,我假设编译器自动生成一个代码,指向另一个隐藏变量的引用。但我真的不确定。
答案 0 :(得分:4)
不,这不安全。更确切地说,这是UB,意味着一切皆有可能。
当您将123.0 + 2.0
传递给Foo
的构造函数时,将构造一个临时double
并绑定到参数fx
。在完整表达(即Foo p(123.0 + 2.0);
)之后,临时将被销毁,然后引用成员f
将被悬空。
请注意,temporary's lifetime不会延长到参考成员f
的生命周期。
一般情况下,临时的生命周期不能通过"传递它来进一步延长:第二个引用,从临时绑定的引用初始化,不会影响其生命周期。
根据标准,$ 15.6.2 / 8 Initializing bases and members [class.base.init]
绑定到a中引用成员的临时表达式 mem-initializer是不正确的。 [实施例:
struct A { A() : v(42) { } // error const int& v; };
- 结束示例]
答案 1 :(得分:3)
啊,调试未定义行为的乐趣。编译器可能会将无效代码编译成工具无法再检测到它无效的东西,这就是这里发生的事情。但它根本没有崩溃。我也使用valgrind来测试程序,但没有出现错误或警告。
从操作系统的角度来看,从valgrind的角度来看,f
引用的内存仍然有效,因此它不会崩溃,而valgrind也不会报错。您看到输出值0
这一事实意味着编译器在您的情况下重新使用以前用于临时对象的内存来存储其他一些不相关的值。
应该很清楚,通过对已经删除的对象的引用来访问该无关值的尝试是无效的。
答案 2 :(得分:2)
是的,只要引用仅用于"临时"的生命周期。变量还没有结束。在您发布的代码中,您将继续引用引用对象的生命周期。 (即不好)
不,那不太正常。
在我的机器上,您在主要打印125中的打印声明而不是0,所以首先让我们重复您的结果:
#include <alloca.h>
#include <cstring>
#include <iostream>
struct Foo
{
double const& f;
Foo(double const& fx) : f(fx)
{
std::cout << fx << " " << this->f << std::endl;
}
double GetF() const
{
return f;
}
};
Foo make_foo()
{
return Foo(123.0 + 2.0);
}
int main()
{
Foo p = make_foo();
void * const stack = alloca(1024);
std::memset(stack, 0, 1024);
std::cout << p.GetF() << std::endl;
return 0;
}
现在打印0!
125.0和2.0是floating point literals。它们的总和是rvalue,在构造Foo对象期间是materialized,因为Foo的构造函数需要引用double。该临时双重存在于堆栈的内存中。
引用是usually implemented来保存它们引用的对象的机器地址,这意味着Foo的引用成员正在保存堆栈内存地址。在调用Foo的构造函数时,该地址处存在的对象在构造函数完成后不存在。
在我的机器上,当临时的生命周期结束时,堆栈内存不会自动归零,因此在代码中引用返回(前)对象的值。在我的代码中,当我重用先前由临时(通过alloca和memset)占用的堆栈内存时,该内存被(正确地)覆盖,并且将来使用该引用反映了该地址的内存状态,该状态不再具有任何内容与临时的关系。在这两种情况下,内存地址都是有效的,因此不会触发段错误。
我添加了make_foo并使用了alloca和std :: memset,因为它有一些特定于编译器的行为,因此我可以使用直观的名称&#34; stack&#34;,但我可以轻松地完成这个而不是实现类似的结果:
Foo p = Foo(123.0 + 2.0);
std::vector<unsigned char> v(1024, 0);
std::cout << p.GetF() << std::endl;
答案 3 :(得分:1)
这确实不安全(它有未定义的行为),而asan AddressSanitizerUseAfterScope会检测到这一点:
$ g++ -ggdb3 a.cpp -fsanitize=address -fsanitize-address-use-after-scope && ./a.out
125.000000 125.000000
=================================================================
==11748==ERROR: AddressSanitizer: stack-use-after-scope on address 0x7fff1bbfdab0 at pc 0x000000400b80 bp 0x7fff1bbfda20 sp 0x7fff1bbfda18
READ of size 8 at 0x7fff1bbfdab0 thread T0
#0 0x400b7f in Foo::GetF() const a.cpp:12
#1 0x4009ca in main a.cpp:18
#2 0x7fac0bd05d5c in __libc_start_main (/lib64/libc.so.6+0x1ed5c)
#3 0x400808 (a.out+0x400808)
Address 0x7fff1bbfdab0 is located in stack of thread T0 at offset 96 in frame
#0 0x4008e6 in main a.cpp:16
This frame has 2 object(s):
[32, 40) 'p'
[96, 104) '<unknown>' <== Memory access at offset 96 is inside this variable
要使用AddressSanitizerUseAfterScope,您需要运行Clang 5.0或gcc 7.1。
Valgrind擅长检测堆内存的无效使用,但由于它在未更改的程序文件上运行,因此通常无法检测堆栈使用错误。
您的代码不安全,因为参数double const& fx
绑定到一个临时的,具有值125.0的具体化的prvalue double。这个临时的生命周期终止于statement-expression Foo p(123.0 + 2.0)
。
使代码安全的一种方法是使用聚合生命周期扩展(Extending temporary's lifetime through rvalue data-member works with aggregate, but not with constructor, why?),删除构造函数Foo::Foo(double const&)
,并更改p
的初始化程序以使用列表初始化语法:
Foo p{123.0 + 2.0};
// ^ ^
答案 4 :(得分:1)
如果临时变量存在于使用引用的位置,则行为已得到很好的定义。在这种情况下,该临时变量的存在正是因为被引用!表格C ++ 11标准第12.2.5节:
引用绑定到的临时文件或 引用绑定到的子对象的完整对象 在参考的有效期内持续存在...
是的,用“ ...”隐藏的单词是“ except”,并且在那里列出了多个例外,但是在本示例情况下,它们都不适用。因此,这是合法且定义明确的,不应发出警告,但不会引起人们的广泛关注。
答案 5 :(得分:0)
如果临时变量存在于使用引用的位置,则行为定义良好。
如果临时在使用引用之前不存在,那么使用引用的行为是不确定的。
不幸的是,您的代码就是后者的一个例子。当语句123.0 + 2.0
结束时,保存Foo p(123.0 + 2.0)
结果的临时值不再存在。然后,下一个语句printf("%f\n", p.GetF())
将访问对该临时语句的引用,该引用不再存在。
一般来说,未定义的行为被认为是不安全的 - 这意味着不需要代码实际执行的操作。您在测试中看到的结果无法保证。