Is there a C++ warning for returning a reference into a temporary?

470 views Asked by At

There's an error for this case:

const int& foo() {
    const int x = 0;
    return x;
}

and even

const int& foo() {
    const std::pair<int,int> x = {0,0};
    return x.first;
}

but not this:

const int& foo() {
    const std::array<int,1> x = {0};
    return x[0];
}

and (less surprisingly) not this:

const int& foo() {
    const std::vector<int> x = {0};
    return x[0];
}

Particularly in the std::vector case, I get that this warning would be pretty tricky, since it's not obvious to the compiler that the const int& returned by std::vector<int>::operator[](size_t) const is a reference into the temporary. I'm actually a little surprised that std::array doesn't fail, though, since this similar case does give me an error:

struct X {
    int x[0];
};

const int& foo() {
    X x;
    return x.x[0];
}

Do any of the popular compilers have a warning/error that can catch these cases? I could imagine a conservative version that would warn about returning a reference that came from a member-function call on a temporary.

I tripped over this with something like the following, in which I inlined a chained series of calls, but because C++ lets you assign locals to const&, the verbose version works while the superficially-identical version deletes the temporary right away, leaving a dangling reference:

#include <iostream>

struct A {
    int x = 1234;
    A() { std::cout << "A::A " << this << std::endl; }
    ~A() { x = -1; std::cout << "A::~A " << this << std::endl; }
    const int& get() const { return x; }
};

struct C { 
    C() { std::cout << "C::C " << this << std::endl; }
    ~C() { std::cout << "C::~C " << this << std::endl; }
    A a() { return A(); }
};

int foo() {
    C c;
    const auto& a = c.a();
    const auto& x = a.get();
    std::cout << "c.a(); a.get() returning at " << &x << std::endl;
    return x;
}

int bar() {
    C c;
    const int& x = c.a().get();
    std::cout << "c.a().get() returning at " << &x << std::endl;
    return x;
}

int main() {
    std::cout << foo() << std::endl;
    std::cout << bar() << std::endl;
}

That outputs

C::C 0x7ffeeef2cb68
A::A 0x7ffeeef2cb58
c.a(); a.get() returning at 0x7ffeeef2cb58
A::~A 0x7ffeeef2cb58
C::~C 0x7ffeeef2cb68
1234
C::C 0x7ffeeef2cb68
A::A 0x7ffeeef2cb58
A::~A 0x7ffeeef2cb58
c.a().get() returning at 0x7ffeeef2cb58
C::~C 0x7ffeeef2cb68
-1
2

There are 2 answers

0
rustyx On

Using a value whose life has ended is undefined behavior, see [basic.life]/6.1. The standard does not require the compiler to output any diagnostics for UB.

So the diagnostics you're seeing are just a courtesy of the compiler. It's nice to see you're getting some of those, but they are certainly far from watertight as you have noticed.

And yes, lifetime extension isn't chainable. That makes it very dangerous and unreliable.

You can try Clang's Address Sanitizer (ASAN).

In fact ASAN seems to be catching your issue (-fsanitize-address-use-after-scope):

==35463==ERROR: AddressSanitizer: stack-use-after-scope on address 0x7fffffffe970 at pc 0x000000498d53 bp 0x7fffffffe910 sp 0x7fffffffe908
READ of size 4 at 0x7fffffffe970 thread T0
2
Slava On

the verbose version works while the superficially-identical version deletes the temporary right away, leaving a dangling reference

Your code is not identical at all. At the first case:

const auto& a = c.a();
const auto& x = a.get();

lifetime of temporary extended for lifetime of const reference, so x is valid as long as a is valid, but in the second:

const int& x = c.a().get();

you have dangling reference x. And the case you have here is unrelated to examples you shown before - when you return a dangling reference to a local variable hence warnings you are looking in examples almost unrelated if a compiler would detect situation you described in real code.

Solution for your case though can be made by designer of the class A:

struct A {
    int x = 1234;
    A() { std::cout << "A::A " << this << std::endl; }
    ~A() { x = -1; std::cout << "A::~A " << this << std::endl; }
    const int& get() const & { return x; }
    int get() && { return x; } // prevent dangling reference or delete it to prevent compilation
};