繁体   English   中英

将 const 引用成员设置为临时变量是否安全?

[英]Is it safe to make a const reference member to a temporary variable?

我试过多次这样编码:

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来测试程序,但没有出现错误或警告。 所以,我假设编译器自动生成了一个代码,将引用指向另一个隐藏变量。 但我真的不确定。

不,这不安全。 更准确地说,这是UB ,意味着一切皆有可能。

当您将123.0 + 2.0传递给Foo的构造函数时,将构造一个临时double 123.0 + 2.0并将其绑定到参数fx 在完整表达式(即Foo p(123.0 + 2.0); )之后临时将被销毁,然后引用成员f将变为悬空。

请注意, 临时对象的生命周期不会延长到引用成员f的生命周期。

通常,不能通过“传递”来进一步延长临时文件的生命周期:从临时文件绑定的引用初始化的第二个引用不会影响其生命周期。

从标准, [class.base.init]/8

绑定到内存初始值设定项中的引用成员的临时表达式格式错误。 [ 例子:

 struct A { A() : v(42) { } // error const int& v; };

— 结束示例 ]

但它根本不会崩溃。 我还使用 valgrind 来测试程序,但没有出现错误或警告。

啊,调试未定义行为的乐趣。 编译器可能会将无效代码编译为工具无法再检测到无效代码的内容,这就是这里发生的情况。

从操作系统的角度来看,从 valgrind 的角度来看, f引用的内存仍然有效,因此它不会崩溃,并且 valgrind 不会报告任何错误。 您看到输出值为0的事实意味着编译器在您的情况下重新使用了以前用于临时对象的内存来存储其他一些不相关的值。

应该清楚的是,通过对已删除对象的引用来访问该不相关值的尝试是无效的。

将 const 引用成员设置为临时变量是否安全?

是的,只要引用仅在“临时”变量的生命周期尚未结束时使用。 在您发布的代码中,您在引用对象的生命周期后一直保持引用。 (即不好)

所以,我假设编译器自动生成了一个代码,将引用指向另一个隐藏变量。

不,这不是正在发生的事情。

在我的机器上,你的主打印语句打印 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 是浮点文字 它们的和一个右值物化的Foo对象的施工过程中,由于Foo的构造函数需要一个双重的参考。 该临时双精度存在于堆栈的内存中。

引用通常被实现来保存它们引用的对象的机器地址,这意味着 Foo 的引用成员保存着一个堆栈内存地址。 调用 Foo 的构造函数时存在于该地址的对象,在构造函数完成后不存在。

在我的机器上,当临时的生命周期结束时,堆栈内存不会自动清零,因此在您的代码中,引用返回(前)对象的值。 在我的代码中,当我重用之前由临时(通过 alloca 和 memset)占用的堆栈内存时,该内存被(正确)覆盖,并且引用的未来使用反映了地址处的内存状态,该地址不再有任何与临时的关系。 在这两种情况下,内存地址都是有效的,因此不会触发段错误。


由于某些特定于编译器的行为,我添加了 make_foo 并使用了 alloca 和 std::memset,因此我可以使用直观的名称“stack”,但我可以同样轻松地完成此操作,从而获得类似的结果:

Foo p = Foo(123.0 + 2.0);
std::vector<unsigned char> v(1024, 0);
std::cout << p.GetF() << std::endl;

这确实是不安全的(它具有未定义的行为),并且 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 的物化纯右值 double。 这个临时的生命周期在语句表达式Foo p(123.0 + 2.0)的末尾终止。

使您的代码安全的一种方法是使用聚合生命周期扩展( 通过右值数据成员扩展临时的生命周期适用于聚合,但不适用于构造函数,为什么? ),通过删除构造函数Foo::Foo(double const&) ,并更改p的初始值设定项以使用列表初始化语法:

Foo p{123.0 + 2.0};
//   ^           ^

如果临时变量存在于使用引用的位置,则行为定义良好。 在这种情况下,这个临时变量的存在正是因为它被引用了! 表格 C++11 标准第 12.2.5 节:

引用绑定到的临时对象或作为引用绑定到的子对象的完整对象的临时对象在引用的生命周期内持续存在......

是的,被 '...' 隐藏的词是“except”,那里列出了多个例外,但在这个例子中没有一个是适用的。 因此,这是合法且定义明确的,不应产生警告,但不是广为人知的极端情况。

如果临时变量存在于使用引用的位置,则行为定义良好。

如果在使用引用之前临时停止存在,则使用引用的行为是未定义的。

不幸的是,您的代码是后者的一个例子。 当语句Foo p(123.0 + 2.0)结束时,保存123.0 + 2.0结果的临时对象不复存在。 下一个语句printf("%f\\n", p.GetF())然后访问对不再存在的临时对象的引用。

一般来说,未定义的行为被认为是不安全的——这意味着对代码实际做什么没有要求。 不能保证您在测试中看到的结果。

正如其他人所说,它目前是不安全的。 所以应该在编译时检查它。 因此,当存储引用时,还应该禁止右值:

    Foo(double &&)=delete;

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM