Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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

like image 701
JVApen Avatar asked Dec 14 '20 09:12

JVApen


2 Answers

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.

like image 160
Amir Kirsh Avatar answered Oct 20 '22 00:10

Amir Kirsh


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.

like image 23
Fedor Avatar answered Oct 19 '22 22:10

Fedor