2
votes

I'm trying to use unique_ptr for a pimpl idiom. So I'm declaring a destructor inside the class so the unique_ptr deletion is not instantiated where the impl class is not defined, and then I define it in another file.

This is my layout:

wrapper.h:

#pragma once
#include <memory>

struct Wrapper
{
    class Impl;
    ~Wrapper();
    std::unique_ptr<Impl> _impl;
};

wrapper.cpp:

#include "wrapper.h"

class Wrapper::Impl {};
Wrapper::~Wrapper() = default;

This file compiles just fine. However when compiling main.cpp I get incomplete type errors (see errors below):

main.cpp:

#include "wrapper.h"

int main()
{
    Wrapper w;
    return 0;
}

However, if I add the two lines from wrapper.cpp at the end of main.cpp, it compiles just fine. I don't understand two things:

  1. Why does the error happen in the first place? I thought declaring the destructor in the class moves the point of delete invocation?
  2. How come adding code at the end of the main.cpp file affects this error? I though "If the default deleter is used, T must be complete at the point in code where the deleter is invoked, which happens in the destructor" (from cppreference).

What am I missing?

UPDATE:

Following @AdrianMole's suggestion I added a ctor declaration inside class Wrapper definition and for some reason it fixed the error, even though the error (and the unique_ptr spec) refers to the destructor.

Updated wrapper.h:

struct Wrapper
{
    class Impl;
    Wrapper();
    ~Wrapper();
    std::unique_ptr<Impl> _impl;
};

So I'm adding a question:

  1. Why does adding a constructor declaration fixes this?

These are the errors I get with MSVC, but similar errors happen with clang or gcc (I tried online compilers):

memory(2536,1): error C2027: use of undefined type 'Wrapper::Impl'

wrapper.h(7): message : see declaration of 'Wrapper::Impl'

memory(2535): message : while compiling class template member function 'void std::default_deleteWrapper::Impl::operator ()(_Ty *) noexcept const'

    with

    [

        _Ty=Wrapper::Impl

    ]

memory(2647): message : see reference to function template instantiation 'void std::default_deleteWrapper::Impl::operator ()(_Ty *) noexcept const' being compiled

    with

    [

        _Ty=Wrapper::Impl

    ]

memory(2574): message : see reference to class template instantiation 'std::default_deleteWrapper::Impl' being compiled

wrapper.h(9): message : see reference to class template instantiation 'std::unique_ptrWrapper::Impl,std::default_delete<Wrapper::Impl>' being compiled

memory(2536,25): error C2338: can't delete an incomplete type

memory(2537,1): warning C4150: deletion of pointer to incomplete type 'Wrapper::Impl'; no destructor called

wrapper.h(7): message : see declaration of 'Wrapper::Impl'

1
Shouldn't you have Wrapper::Wrapper() : _impl(new Impl) {} too in wrapper.cpp ?m88
@m88 It's not needed for compilation, so I left out the construction code. In any case, in the real life use case it's not done in the constructor but after receiving remote parameters.Asaf
The problem is not with where you define the Wrapper destructor but where you define the Impl class. I guess the default constructor for the std::unique_ptr<Impl> object needs to know the full definition of Impl.Adrian Mole
@AdrianMole thanks, but actually unique_ptr doesn't need the full implementation of the type on default constructor (more details in the cppreference link in the question). And it doesn't explain why adding the definition after the code fixes it.Asaf
Curious. The clang-cl compiler gives this: error : invalid application of 'sizeof' to an incomplete type 'Wrapper::Impl' ... in instantiation of member function 'std::unique_ptr<Wrapper::Impl>::~unique_ptr' requested here (So I guess it's the default destructor of the unique_ptr object.) But yeah - not sure why adding the definition after the code fixes that.Adrian Mole

1 Answers

1
votes

Any constructor definition (including the implicitly defined default constructor) potentially invokes the destructor of the types of all member objects of class type. The rationale for this is that if constructing a later member throws, the destructor of all previous members would need to be called. In the case of the last member, this is not strictly required, but the standard does not make this exception.

For example, if your class had been:

struct complete_type_with_throwing_constructor {
    complete_type_with_throwing_constructor() { throw 0; }
};

struct Wrapper
{
    class Impl;
    ~Wrapper();
    std::unique_ptr<Impl> _impl;
    complete_type_with_throwing_constructor x;
};

Then the implicit default constructor of Wrapper would construct _impl then destroy it after the default constructor of x throws.

This is the reason why your code works if you explicitly declare your default constructor. This moves the definition to a point where the destructor of std::unique_ptr<Impl> can be instantiated.


As for your second question, you are right that the code should still not work when adding the definition afterwards. It's just that compilers do not detect it, so they don't have an error.