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

此生再无相见时 提交于 2021-01-18 19:14:52

问题


I've tried to code like this several times:

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;
}

But it doesn't crash at all. I've also used valgrind to test the program but no error or warning occured. So, I assume that the compiler automatically generated a code directing the reference to another hidden variable. But I'm really not sure.


回答1:


No, this is not safe. More precisely this is UB, means anything is possible.

When you pass 123.0 + 2.0 to the constructor of Foo, a temporary double will be constructed and bound to the parameter fx. The temporary will be destroyed after the full expression (i.e. Foo p(123.0 + 2.0);), then the reference member f will become dangled.

Note that the temporary's lifetime won't be extended to the lifetime of the reference member f.

In general, the lifetime of a temporary cannot be further extended by "passing it on": a second reference, initialized from the reference to which the temporary was bound, does not affect its lifetime.

And from the standard, [class.base.init]/8

A temporary expression bound to a reference member in a mem-initializer is ill-formed. [ Example:

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

— end example ]




回答2:


But it doesn't crash at all. I've also used valgrind to test the program but no error or warning occured.

Ah, the joy of debugging undefined behaviour. It's possible that the compiler compiles invalid code to something where tools can no longer detect that it's invalid, and that's what happens here.

From the OS perspective, and from valgrind's perspective, the memory that f references is still valid, therefore it doesn't crash, and valgrind doesn't report anything wrong. The fact that you see an output value of 0 means the compiler has, in your case, re-used the memory that was formerly used for the temporary object to store some other unrelated value.

It should be clear that attempts to access that unrelated value through a reference to an already-deleted object are invalid.




回答3:


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

Yes, as long as the reference is used only while the lifetime of the "temporary" variable has not ended. In the code you posted, you are holding on to a reference past the lifetime of the referenced object. (i.e. not good)

So, I assume that the compiler automatically generated a code directing the reference to another hidden variable.

No, that's not quite what's happening.

On my machine your print statement in main prints 125 instead of 0, so first let's duplicate your results:

#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;
}

Now it prints 0!


125.0 and 2.0 are floating point literals. Their sum is a rvalue that is materialized during the construction of the Foo object, since Foo's constructor requires a reference to a double. That temporary double exists in memory on the stack.

References are usually implemented to hold the machine address of the object they reference, which means Foo's reference member is holding a stack memory address. The object that exists at that address when Foo's constructor is called, does not exist after the constructor completes.

On my machine, that stack memory is not automatically zeroed when the lifetime of the temporary ends, so in your code the reference returns the (former) object's value. In my code, when I reuse the stack memory previously occupied by the temporary (via alloca and memset), that memory is (correctly) overwritten and future uses of the reference reflect the state of the memory at the address, which no longer has any relationship to the temporary. In both cases the memory address is valid, so no segfault is triggered.


I added make_foo and used alloca and std::memset because of some compiler-specific behavior and so I could use the intuitive name "stack", but I could have just as easily done this instead which achieves similar results:

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



回答4:


This is indeed unsafe (it has undefined behavior), and the asan AddressSanitizerUseAfterScope will detect this:

$ 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

In order to use AddressSanitizerUseAfterScope, you need to run Clang 5.0 or gcc 7.1.

Valgrind is good at detecting invalid use of heap memory, but because it runs on an unaltered program file it cannot in general detect stack use bugs.

Your code is unsafe because the parameter double const& fx is bound to a temporary, a materialized prvalue double with value 125.0. This temporary has lifetime terminating at the end of the statement-expression Foo p(123.0 + 2.0).

One way to make your code safe is to use aggregate lifetime extension (Extending temporary's lifetime through rvalue data-member works with aggregate, but not with constructor, why?), by removing the constructor Foo::Foo(double const&), and changing the initializer of p to use the list-initialization syntax:

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



回答5:


If the temporary variable exists at the point where the reference is used, then the behavior is well defined. And in this case this temporary variable exists exactly because it is referenced! Form C++11 standard section 12.2.5:

The temporary to which the reference is bound or the temporary that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference ...

Yes, the word hidden by '...' is the "except" and multiple exceptions are listed there, but none of them are applicable in this example case. So this is legal and well defined, should produce no warnings, but not very widely known corner case.




回答6:


If the temporary variable exists at the point where the reference is used, then the behaviour is well defined.

If the temporary ceases to exist before the reference is used, then the behaviour of using the reference is undefined.

Unfortunately, your code is an example of the latter. The temporary which holds the result of 123.0 + 2.0 ceases to exist when the statement Foo p(123.0 + 2.0) finishes. The next statement printf("%f\n", p.GetF()) then accesses a reference to that temporary which no longer exists.

Generally speaking, undefined behaviour is considered unsafe - it means there is no requirement on what the code actually does. The result you are seeing in testing is not guaranteed.



来源:https://stackoverflow.com/questions/45385433/is-it-safe-to-make-a-const-reference-member-to-a-temporary-variable

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!