11

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

JVApen
  • 11,008
  • 5
  • 31
  • 67
  • Note that making `ForwardDeclared` a complete type [fixes the compilation problem](https://godbolt.org/z/YGPKex). – Sergey Kalinichenko Dec 14 '20 at 09:43
  • @SergeyKalinichenko I know, however, that also increases the amount of code that needs to be included – JVApen Dec 14 '20 at 10:28
  • 1
    Maybe [a related bug](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=60497). – xskxzr Dec 14 '20 at 11:25
  • 1
    @xskxzr Interesting bug, however, it looks different at first sight. In the first example it uses a customer deletor. This bug is also logged in libstdc++, while MSVC and Clang use a different standard library. (I'm forcing libc++ for clang) – JVApen Dec 14 '20 at 15:37
  • I rechecked your issue (since my first comment was wrong...) and I do not think, that it's the kind of bug that xskxzr linked here - I still doubt it's a bug at all (happens almost never that clang and gcc are both wrong while MSVC is fine...). I do not fully understand, how godbolt handles incomplete symbols.Is there a hidden setting for that? And did you reproduce this issue with a real project? PS: Isn't m_ptr2 actually direct initialization? – Secundi Dec 14 '20 at 21:16
  • It doesn't need to link for compiler explorer, I've reproduced this with MSVC and Clang-cl on real code. (This is just a reduced form of it) – JVApen Dec 14 '20 at 22:33
  • I'm also not sure its a bug in clang, however, I would like to understand it – JVApen Dec 14 '20 at 22:34
  • Include all your compiler versions. – Asteroids With Wings Dec 14 '20 at 22:38
  • @asteroidswithwings I don't think the exact versions are relevant, using the latest available, trunk for GCC and clang and 2019 for MSVC, for the details, see compiler explorer link – JVApen Dec 15 '20 at 07:11
  • Cannot reproduce your issue with clang. Compiles fine for me for several latest compiler versions (as expected). Can you ensure from your compiler output that the instantiation origin is a valid location where the definition of ForwardDeclared is available? – Secundi Dec 15 '20 at 08:26
  • @JVApen You have no idea whether they're relevant. Always provide them. Help us to help you. And remember that your question lasts forever: what is trunk today may not be trunk tomorrow. – Asteroids With Wings Dec 15 '20 at 12:30

2 Answers2

2

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.

Amir Kirsh
  • 12,564
  • 41
  • 74
  • Thanks for the answer, I've unmarked the question as solved for now and logged a bug with clang about it: https://github.com/llvm/llvm-project/issues/54291 – JVApen Mar 09 '22 at 07:27
1

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.

Fedor
  • 17,146
  • 13
  • 40
  • 131
  • In the case of a return value the destructor is potentially invoked according to https://timsong-cpp.github.io/cppwp/n4868/stmt.return#2.sentence-7, but in the rule for copy elision from prvalue initializers of same type I don't see any equivalent requirement: https://timsong-cpp.github.io/cppwp/n4868/dcl.init#general-16.6.1. cppreference also mentions this explicitly in the case of copy elision in return statements. – user17732522 Mar 09 '22 at 00:19