0

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!).

  • 1
    @NathanOliver They actually do when there is no constructor involved, that is, when the class is of an aggregate type: [link](https://stackoverflow.com/questions/35313292/aggregate-reference-member-and-temporary-lifetime) – TiredQuarenteneer Jun 15 '22 at 20:02
  • 1
    Oh neat. Comment removed. – NathanOliver Jun 15 '22 at 20:07
  • Whichever compiler is actually correct, I have to ask: is it _wise_ to write code like this? You're asking a lot of the compiler [writers] here. – Paul Sanders Jun 15 '22 at 20:16
  • I am expecting only standard behavior, C++17 standard for that. If it is wise or not, I am not fully decided yet. – TiredQuarenteneer Jun 15 '22 at 20:31

1 Answers1

1

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.

user17732522
  • 53,019
  • 2
  • 56
  • 105
  • 1
    Thank you for your time, I am aware of the danger associated with this and I will probably implement a non-aggregate type that should only ever exist as a temporary, this way it will always not work if used in a way to extend lifetimes... my main concern is with the future of the language as rules on this topic are somehow known to change. I will make a report on the issue though. – TiredQuarenteneer Jun 15 '22 at 21:00