Home > Software design >  Requirements on returned type that may have some member functions SFINAE'd away in the function
Requirements on returned type that may have some member functions SFINAE'd away in the function

Time:04-08

Refining from Why is the destructor implicitly called?

My understanding of calling convention is that functions construct their result where the caller asked them to (or in a conventional place?). With that in mind, this surprises me:

#include <memory>

struct X; // Incomplete type.

// Placement-new a null unique_ptr in-place:
void constructAt(std::unique_ptr<X>* ptr) { new (&ptr) std::unique_ptr<X>{nullptr}; }

// Return a null unique_ptr:
std::unique_ptr<X> foo() { return std::unique_ptr<X>{nullptr}; }

https://godbolt.org/z/rqb1fKq3x Whereas constructAt compiles, happily placement-newing a null unique_ptr<X>, foo() doesn't compile because the compiler wants to instantiate unique_ptr<X>::~unique_ptr(). I understand why it can't instantiate that destructor (because as far as the language is concerned, it needs to follow the non-nullptr branch of the d'tor that then deletes the memory [https://stackoverflow.com/questions/28521950/why-does-unique-ptrtunique-ptr-need-the-definition-of-t]). Basically without a complete X, the unique_ptr's destructor is SFINAE'd away (right?). But why does a function returning a value have to know how to destruct that value? Isn't the caller the one that will have to destruct it?

Clearly my constructAt and foo functions aren't morally equivalent. Is this language pedantry, or is there some code path (exceptions?) where foo() would have to destruct that value?

CodePudding user response:

In your specific case there is no way that the destructor may be invoked. However, the standard specifies the situations in which the destructor is potentially invoked in more general terms. If a destructor is potentially invoked it requires a definition (even if there is no path that could call it) and this will therefore cause implicit instantiation which fails in your case since instantiation of the std::unique_ptr<X> destructor requires X to be complete.

In particular the destructor is potentially invoked for every result object in a return statement.

I think the reason for this choice is described in CWG issue 2176: In general there may be local variables and temporaries in the function which are destroyed after the result object of the return statement has been constructed. But if the destruction of one of these objects throws an exception, then the already constructed result object should also be destroyed. This requires the destructor to be defined.

CWG issue 2426 then made the destructor potentially invoked even if there is no actual invocation due to the above reasoning, in line with implementations. I assume this choice was made simply because it doesn't require any additional decision making on the compiler's part and was already implemented.

  • Related