Why does copy initializaton require destructor in C++17 with guaranteed move/copy elision?

405 views Asked by At

The following code compiles with MSVC (/permissive-) and fails to compile with GCC/Clang for m_ptr1 and m_ptr2.

#include <memory>

struct ForwardDeclared;

class A {
    public:
        explicit A();
        ~A();
    private:
        std::unique_ptr<ForwardDeclared> m_ptr1 = nullptr;    // not ok
        std::unique_ptr<ForwardDeclared> m_ptr2 {std::unique_ptr<ForwardDeclared>{}};    // not ok
        std::unique_ptr<ForwardDeclared> m_ptr3 {nullptr};    // ok 
        std::unique_ptr<ForwardDeclared> m_ptr4;              // ok
};

int main() {
    A a;
    return 0;
}

Code at compiler-explorer

My understanding is that the = sign results in copy initialization, however, thanks to copy elision I would expect m_ptr2 would still be initialized without failure.

Why does m_ptr2 require a destructor of ForwardDeclared and are Clang/GCC correct for this? (Bonus: Is it correct to conclude that m_ptr1 is incorrectly accepted by MSVC?)

EDIT: Logged a bug with clang about this issue: https://github.com/llvm/llvm-project/issues/54291

2

There are 2 answers

1
Amir Kirsh On

CWG 2426 refers specifically to a destructor being potentially invoked in the cases:

A destructor is potentially invoked if it is invoked or as specified in 7.6.2.8 [expr.new], 8.7.4 [stmt.return], 9.4.2 [dcl.init.aggr], 11.9.3 [class.base.init], and 14.2 [except.throw]. A program is ill-formed if a destructor that is potentially invoked is deleted or not accessible from the context of the invocation.

Section 11.9.2 [class.expl.init] is not mentioned in the above list, and thus seems not to be in the destructor is potentially invoked case.

Which raises a question whether indeed as @Fedor argues, MSVC is wrong and GCC and Clang are correct. On the face of it, it seems to be the opposite.

Note that cppreference on copy elision doesn't argue that all cases of copy elision require the destructor to be visible, it refers only to the case of:

In a return statement, when the operand is a prvalue of the same class type (ignoring cv-qualification) as the function return type [...] The destructor of the type returned must be accessible at the point of the return statement and non-deleted, even though no T object is destroyed.

1
Fedor On

My understanding is that the = sign results in copy initialization, however, thanks to copy elision I would expect m_ptr2 would still be initialized without failure.

Copy elision requires the destructor of the type be accessible and non-deleted, even though no object is destroyed, see https://en.cppreference.com/w/cpp/language/copy_elision

So GCC and Clang correctly check the destructor, which is not valid for the incomplete type ForwardDeclared.

Bonus: Is it correct to conclude that m_ptr1 is incorrectly accepted by MSVC?

Yes, MSVC is incorrect here.

See Why is public destructor necessary for mandatory RVO in C++? for an explanation on why mandatory copy-elision doesn't apply.

Why does m_ptr2 require a destructor of ForwardDeclared and are Clang/GCC correct for this?

The same reasoning applies here about the necessity of valid destructor for the copy elision.