Why does = default member initializer request instantiation of unique_ptr destructor while {} does not?
Asked Answered
P

1

16

This is a follow up of this question: Does PIMPL idiom actually work using std::unique_ptr?

The full example uses multiple files, so for the sake of this question I will reduce it here. The full working example is here: https://wandbox.org/permlink/AepAJYkbRU4buDoJ and the full non-working example here: https://wandbox.org/permlink/0kP23UYJbSaUvJgS.

The shorter example is:

#include <memory>

struct A;

struct B {
    ~B();
    std::unique_ptr<A> p =  nullptr;
};

Which produces the error:

In file included from <source>:1:
In file included from /opt/compiler-explorer/gcc-11.2.0/lib/gcc/x86_64-linux-gnu/11.2.0/../../../../include/c++/11.2.0/memory:76:
/opt/compiler-explorer/gcc-11.2.0/lib/gcc/x86_64-linux-gnu/11.2.0/../../../../include/c++/11.2.0/bits/unique_ptr.h:83:16: error: invalid application of 'sizeof' to an incomplete type 'A'
        static_assert(sizeof(_Tp)>0,
                      ^~~~~~~~~~~
/opt/compiler-explorer/gcc-11.2.0/lib/gcc/x86_64-linux-gnu/11.2.0/../../../../include/c++/11.2.0/bits/unique_ptr.h:361:4: note: in instantiation of member function 'std::default_delete<A>::operator()' requested here
          get_deleter()(std::move(__ptr));
          ^
<source>:7:29: note: in instantiation of member function 'std::unique_ptr<A>::~unique_ptr' requested here
    std::unique_ptr<A> p =  nullptr;
                            ^
<source>:3:8: note: forward declaration of 'A'
struct A;
       ^
1 error generated.

While using {} compiles:

#include <memory>

struct A;

struct B {
    ~B();
    std::unique_ptr<A> p{nullptr};
};

No errors: https://godbolt.org/z/snsfsjdqE

What is the difference? Why does std::unique_ptr<A> p = nullptr; require to instantiate the unique_ptr destructor, but std::unique_ptr<A> p{nullptr}; does not?

PS: I used clang for a clearer error message. Same results with gcc. All versions I tried so far.

Placeman answered 8/3, 2022 at 15:27 Comment(8)
RelatedMickiemickle
@CoryKramer looks like a duplicate, though the answer states that newer versions of gcc are fine with it. I tested with clang 13 and gcc 11.2Placeman
@CoryKramer also not ok with gcc 9.2 godbolt.org/z/nej89vGev :/Placeman
Actually, the way I read the comments to that answer that Kramer linked, it was said to be fixed in gcc 9.2 only because the test confirming that was not correct (due to limitations, at that time of the online compiler used).Bustamante
@Bustamante oh ok, when A is complete then there is no issue to begin with. Then it isnt a good duplicatePlaceman
MSVC (latest) compiles it fine. Clang and GCC do not.Solar
Which C++ standard are you specifying your compilers use when testing?Cruelty
Does this answer your question? Why does copy initializaton require destructor in C++17 with guaranteed move/copy elision?Jazzy
E
1

It seems to be a language issue unrelated to std::unique_ptr. I would say a bug in both GCC and Clang.

Here it is without std::unique_ptr.

This one compiles:

template<typename T>
struct uptr {
    uptr(nullptr_t) {}
    ~uptr() {
        delete (new T);
    }
};

class A
{
public:
  A();
  ~A();
private:
  class B;
  uptr<B> m_b {nullptr}; // the dtor of uptr is not instantiated yet
};

But this doesn't compile:

template<typename T>
struct uptr {
    uptr(nullptr_t) {}
    ~uptr() {
        delete (new T);
    }
};

class A
{
public:
  A();
  ~A();
private:
  class B;
  uptr<B> m_b = nullptr; // the dtor of uptr tries to be instantiated here
};

My intuition is that the instantiation of uptr's destructor shall be delayed to the instantiation of A's destructor, in both cases. So both cases shall pass compilation. This is why I believe it is a bug in the compiler, trying to instantiate the destructor too early, before A's destructor appears and when B is still incomplete, in the second case.

It is interesting to note that with the curly brackets most compilers (GCC, Clang, MSVC) indeed delay the instantiation of the destructor. Only ICC in my check rejects the code - wrongly, I would say. However, if constructing with assignment sign most compilers (GCC, Clang, ICC) instantiate the destructor too early, only MSVC in my check still delays it - as it should, I believe. In case we use both the curly brackets and assignment sign, Clang would join MSVC, delaying the destructor instantiation. GCC and ICC would still try to instantiate it.

See also a related SO post that explains why I believe GCC and Clang are wrong here.

Extinct answered 8/3, 2022 at 22:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.