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

Posted

技术标签:

【中文标题】将 const 引用成员设为临时变量是不是安全?【英文标题】:Is it safe to make a const reference member to a temporary variable?将 const 引用成员设为临时变量是否安全? 【发布时间】:2017-07-29 04:56:07 【问题描述】:

我已经尝试过几次这样的编码:

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

【问题讨论】:

但是你传递的不是临时变量,而是常量表达式... 程序可能不会崩溃,因为编译器决定将常量125.0 somwhere 存储在目标文件中。然而,这并不能保证会发生。 【参考方案1】:

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

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

请注意,temporary's lifetime 不会延长到引用成员 f 的生命周期。

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

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

绑定到一个引用成员的临时表达式 mem-initializer 格式不正确。 [ 示例:

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

— 结束示例 ]

【讨论】:

【参考方案2】:

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

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

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

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

【讨论】:

【参考方案3】:

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

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

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

不,情况并非如此。

在我的机器上,你在 main 中的 print 语句打印的是 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 的引用。该临时 double 存在于堆栈的内存中。

引用是usually implemented 来保存它们引用的对象的机器地址,这意味着 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;

【讨论】:

【参考方案4】:

这确实是不安全的(它具有未定义的行为),并且 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&amp; fx 绑定到一个临时的、物化纯右值双精度值125.0。这个临时的生命周期在语句表达式 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&amp;),并将 p 的初始化程序更改为使用列表初始化语法:

Foo p123.0 + 2.0;
//   ^           ^

【讨论】:

【参考方案5】:

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

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

是的,被 '...' 隐藏的单词是“例外”,其中列出了多个例外,但在此示例中它们都不适用。所以这是合法且定义明确的,应该不会产生警告,但不是广为人知的极端情况。

【讨论】:

【参考方案6】:

如果临时变量存在于使用引用的位置,则行为已明确。

如果在使用引用之前临时不存在,那么使用引用的行为是未定义的。

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

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

【讨论】:

【参考方案7】:

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

    Foo(double &&)=delete;

【讨论】:

以上是关于将 const 引用成员设为临时变量是不是安全?的主要内容,如果未能解决你的问题,请参考以下文章

临时变量不能作为非const引用

临时子表达式的临时生命周期,绑定到引用

C++ - 使用 const 引用来延长一个临时的、好的或 UB 的成员?

const 引用函数参数:是不是可以禁止临时对象?

临时变量作为非const的引用进行参数传递引发的编译错误

类成员引用变量是不是具有内置的“常量正确性”?