The rules of temporary life time extension seem to be simple, but they are not.
Let’s play with them a bit in this riddle: “Extended or Not?”
In this riddle we will present a long list of cases, for each case you shall answer whether the code is legitimate (compiles and has a defined behavior) or not (undefined behavior, dangling ref: k
is used after being dead), and more importantly – why (e.g. what makes k
be still alive or not).
If you wish to cast your answer, go with a case by case, based on the variable names, e.g. (this is just an example, not the correct answers!):
a:
legit, life time is extended because…
b:
undefined, life time is not extended because…
c:
assert fails, not the same address because… but if we put the assert in comment then the code is legit
d:
assert fails, not the same address because… if we put the assert in comment then the code has still undefined behavior as life time is not extended because…
etc.
Are you ready?
Let’s begin:
// a
const std::string& foo1() {
return "hey!";
}
const auto& a = foo1();
std::cout << a << std::endl;
Is the code above legit or not?
(Should it compile and run perfectly? Should not compile? Should compile but may crash? – Why?)
OK, let’s move on, same question for all code snippets:
// b
std::string foo2() {
return "foo2";
}
const auto& b = foo2();
std::cout << b << std::endl;
Let’s add a struct for the next questions:
struct A {
std::string name;
int i[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
static int* last_i;
static std::string* last_n;
A(std::string name1): name(std::move(name1)) {
std::cout << "A:" << name << ' ';
last_i = i;
last_n = &name;
}
std::string foo() const {
using namespace std::string_literals;
return ("A::foo:"s + name);
}
auto foo2() const { return i; }
const auto& foo3() const { return name; }
operator bool() const { return true; }
operator auto&() const { return name; }
auto& operator [](size_t i) const { return name[i]; }
~A() { std::cout << "~A:" << name << std::endl; }
};
// A.cpp
int* A::last_i;
std::string* A::last_n;
Now we are ready for additional questions, using struct A
as defined above:
// c
const auto& c = A("c");
assert(c.i == A::last_i);
assert(&c.name == A::last_n);
std::cout << c.i[9] << std::endl;
std::cout << c.name << std::endl;
// d
const auto& d = A("d").i;
assert(d == A::last_i);
std::cout << d[9] << std::endl;
// e
const auto& e = A("e").i[0];
assert(&e == A::last_i);
std::cout << e << std::endl;
// e2
const double& e2 = A("e2").i[0];
assert((void*)&e2 == A::last_i);
std::cout << e2 << std::endl;
// f
const auto& f = A("f").foo();
assert(&f == A::last_n);
std::cout << f << std::endl;
// g
const auto& g = A("g").foo2();
assert(g == A::last_i);
std::cout << g << std::endl;
std::cout << g[0] << std::endl;
// h1
const auto& h1 = A("h1_cond") ? A{"h1_true"} : A("h1_false");
assert(h1.i == A::last_i);
std::cout << h1 << std::endl;
// h2
const auto& h2 = !A("h2_cond") ? A{"h2_true"} : A("h2_false");
assert(h2.i == A::last_i);
std::cout << h2 << std::endl;
// j
const auto& j = A("I'm j").name;
assert(&j == A::last_n);
std::cout << j << std::endl;
// k
const auto& k = A("I'm k").foo3();
assert(&k == A::last_n);
std::cout << k << std::endl;
// m
const std::string& m = A("m");
assert(&m == A::last_n);
std::cout << m << std::endl;
// n
const auto& n = A("n")[0];
assert(&n == A::last_n->c_str());
std::cout << n << std::endl;
// p
const auto& p = A("I'm p").name[0];
assert(&p == A::last_n->c_str());
std::cout << p << std::endl;
A bonus part. Let’s have now another struct:
struct B {
int&& r;
};
What about these cases? (all are using struct B
).
// b1
B b1{7};
std::cout << b1.r << std::endl;
// b2
B b2 = {7};
std::cout << b2.r << std::endl;
// b3
B b3{.r = 7};
std::cout << b3.r << std::endl;
// b4
– allowed since C++20
// Note 1: compiles in gcc since 10.1 but doesn’t compile yet in clang (checked with clang 14)
// Note 2: compiles doesn’t necessarily mean legit!
B b4(7);
std::cout << b4.r << std::endl;
// b5
– allowed since C++20
// Note 1: compiles in gcc since 10.1 but doesn’t compile yet in clang (checked with clang 14)
// Note 2: compiles doesn’t necessarily mean legit!
int i = 7;
B b5(std::move(i));
std::cout << b5.r << std::endl;
That’s it!
You can start going case by case and cast your vote.