Search code examples
c++c++17lifetimetemporarycopy-elision

Temporary lifetime extension mixed with copy elision object on clang


I have an issue in a project of mine that uses aggregate types to extend the lifetime of temporaries in a relatively safe manner by making aggregates that contain references uncopyable and unmovable, however mandatory copy/move elision (C++17) don't care if an object is copyable or movable. This is all well and good as, in my mind, the copy/move should never really happen as there actually should only be one object. In my case this object has a reference that extends the lifetime of some temporary and, to my knowledge, the temporary should only be destroyed when the aggregate that holds the reference is destroyed.
The following code is a simplified example of the problem, notice that here B is indeed copyable, but it could as well not be and the same result would follow.

#include <iostream>

struct K
{
    K() { std::cout << "K::K()" << std::endl; }
    K(K const&) { std::cout << "K::K(K const&)" << std::endl; }
    K(K&&) { std::cout << "K::K(K&&)" << std::endl; }
    ~K() { std::cout << "K::~K()" << std::endl; }
};

struct B
{
    K const& l;
    ~B() { std::cout << "B::~B()" << std::endl; }
};

int main() {
    B b = B{ K{} };
    std::cout << "end of main" << std::endl;
    (void)b;
}

The code above has different behavior in different compilers. MSVC and GCC will destroy the temporary K{} only after B b, while Clang will destroy K{} at the end of the expression. My question is: Is the code presented here invoking UB? If not, who is correct, MSVC and GCC or Clang? And is this issue known?


Just as a note: to make B not copyable in C++17 it suffices to declare the copy-constructor as deleted and it will still be an aggregate. In C++20 this has changed (don't ask me why) again!... and you need to include a non-copyable member in the aggregate as p1008r1 shows (great solution!).


Solution

  • This looks like a bug in Clang.

    With mandatory copy elision B b = B{ K{} }; should be fully equivalent to B b{K{}}; and lifetime extension of the K object to the lifetime of b applies there since it is aggregate initialization. No other temporary B object exists which could contain a reference which is bound to the temporary K object first and I don't see any exception in the lifetime extension rules that could be relevant.

    There is an exception which applies through the mandatory copy elision in a return statement, so e.g. returning B{K{}} to assign to initialize B b from will not work to extend the lifetime of the K object, but I think it is obvious that this couldn't work.

    I could not find any matching issue on the LLVM issue list at https://github.com/llvm/llvm-project/issues with a quick search. You might want to consider reporting it.

    There was a related CWG issue 1697 asking what the behavior should be prior to C++17 given optional copy elision, but that was closed with the copy elision being made mandatory. I am not sure what the intended behavior is prior to C++17.


    This does sounds kind of dangerous though, since the lifetime may change if someone chooses to compile with -std=c++14 instead and generally the lifetime extension rule for aggregate initialization is kind of non-obvious. In particular it does not apply to C++20 parenthesized aggregate initialization.