14

In Range-based for loop on a temporary range, Barry mentioned that the following is not affected by the destroyed temporary object, and I tested member v indeed exists throughout the for-loop (as the destructor ~X didn't get called throughout the for-loop). What is the explanation?

struct X {
    std::vector<int> v;

    ~X()
    {
    }
};

X foo()
{
    return X();
}

for (auto e : foo().v) {
    // ok!
}
Anning Wuwang
  • 333
  • 1
  • 8
  • Why the doubt? Can you elaborate that more please? The question there is about something completely different? – πάντα ῥεῖ Jun 05 '22 at 19:44
  • 1
    Are you familiar with [temporary lifetime extension](https://en.cppreference.com/w/cpp/language/reference_initialization#Lifetime_of_a_temporary)? This is a rather obscure form of it. – HolyBlackCat Jun 05 '22 at 19:48
  • Thanks, what I'm not clear about is that __range here is assigned to by X.v not X, v should be kept alive by this auto && __range = foo().v, however X() as a temporary object is not assigned to any reference so it should be destroyed after auto && __range = foo().v is executed? if so after it gets deleted as v is it's member value, v should also be deleted? – Anning Wuwang Jun 05 '22 at 19:51
  • 2
    I'm glad I answered this (even though I got the answer wrong), because I learned something. But I agree with @user177's comment below, I wouldn't write code like this. (in any context, not just a range-based for loop). – Paul Sanders Jun 05 '22 at 20:23

2 Answers2

17

This is an obscure form of temporary lifetime extension. Normally you have to bind the temporary directly to the reference for it to work (e.g. for (auto x : foo())), but according to cppreference, this effect propagates through:

  • parentheses ( ) (grouping, not a function call),
  • array access [ ] (not overloaded; must use an array and not a pointer),
  • member access ., .*,
  • ternary operator ? :,
  • comma operator , (not overloaded),
  • any cast that doesn't involve a "user-defined conversion" (presumably uses no constructors nor conversion operators)

I.e. if a.b is bound to a reference, the lifetime of a is extended.

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • 13
    Importantly this applies only through a direct data member access. If there was a e.g. a `auto& getV() { return v; }` member function and then `for (auto e : foo().getV())` the extension wouldn't apply and the loop would indeed use a dangling reference, causing UB. So relying on something like this feels a bit risky. – user17732522 Jun 05 '22 at 20:07
  • 1
    We should probably overload getters for 'r-value this' to return copies or better yet move things, e.g. `auto getV()&& { return std::move(v); }`, as per Nicolai Josuttis' advice, and then it'll probably work) And yet, I agree, relying on this behaviour is risky – Alex Vask Jun 06 '22 at 15:42
  • Please emphasize that only array access, not general indexing, is covered. – Deduplicator Jun 06 '22 at 15:43
1

Temporary lifetime extension is achieved when a reference variable is bonded to a temporary directly, but not only. For the exact list of temporary lifetime extension, see in the specification: [class.temporary].

The answer provided by his holiness @HolyBlackCat is very good, but I feel some examples are required.

⦿ binding a temporary directly

// function prototype
std::string foo(); 

// calling foo:
const auto& b = foo(); // lifetime is extended, directly bind to a temporary

// also, similarly:
const std::string& s = "hi"; // lifetime is extended, the same

According to the language rules, temporary lifetime extension can be also achieved in any of the following cases:

⦿ parentheses ( ) (grouping, not a function call)

const auto& a = (foo()); // lifetime is extended, grouping with parenths is ok 
const std::string& s = ("hello "s + "world"); // lifetime is extended, the same

For the next cases, let's add the following struct:

struct A {
    std::string str = "hey";
    int arr[3] = {2, 3, 4};
    int* ptr = arr;
    const auto& foo() const {
        return str;
    }
};

⦿ member access ., .*

const auto& b1 = A().str; // lifetime of A() is extended
const auto& b2 = A().arr; // lifetime of A() is extended
const auto& b3 = A().ptr; // lifetime of A() is extended
// BUT -
const auto& b4 = *A().ptr; // lifetime of A() is NOT extended (b4 dangling)

// pointer to member access
const auto& str_ptr = &A::str;
const auto& arr_ptr = &A::arr;
const auto& ptr_ptr = &A::ptr;

const auto& c1 = A().*str_ptr; // lifetime of A() is extended
const auto& c2 = A().*arr_ptr; // lifetime of A() is extended
const auto& c3 = A().*ptr_ptr; // lifetime of A() is extended

// BUT - not for a member function
const auto& foo_ptr = &A::foo;
// below initialization is bounded to a function call result
// not to a member access
const auto& c4 = (A().*foo_ptr)(); // lifetime of A() is NOT extended (c4 dangling)

⦿ array access [ ] (not overloaded; must use an array and not a pointer)

const auto& d1 = A().arr[0]; // lifetime of A() is extended
// BUT - not for pointers
// pointer access with []
const auto& d2 = A().ptr[0]; // lifetime of A() is NOT extended (d2 dangling)
// neither for overloaded []
const auto& d3 = A().str[0]; // lifetime of A() is NOT extended (d3 dangling)

⦿ ternary operator ? :

const auto& e1 = true? A() : A(); // lifetime of the 1st A() is extended
const auto& e2 = false? A() : A(); // lifetime of the 2nd A() is extended

⦿ comma operator , (not overloaded)

const auto& f1 = (A(), A()); // lifetime of the 2nd A() is extended

⦿ any cast that doesn't involve a "user-defined conversion" (presumably uses no constructors nor conversion operators)

const auto& g1 = const_cast<const A&&>(A()); // lifetime of A() is extended

const double& g2 = A().arr[0]; // lifetime of A() is NOT extended
                               // but this is a valid ref to a double
                               // converted from an int, as a temporary

For a casting that doesn't extend lifetime, let's add an additional class:

class B {
    const A& a;
public:
    B(const A& a): a(a){}
};

The following casting goes through user-defined casting and thus will not extend the life time of A:

const auto& g3 = ((B&&)A()); // lifetime of A() is NOT extended (g3 dangling)
Amir Kirsh
  • 12,564
  • 41
  • 74