C++ Riddle - extended or not - June-06, 2022

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;

// b4allowed 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;

// b5allowed 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.

1 Like

It is not yet a full answer, but you can see almost all the relevant details for answering the question in the link below. Take a look and try to answer the question with this.