Reputation: 33
This example shows strange behavior of compilers (msvc14, gcc, clang), but I did't find explanation.
When we implement pipml idiom and use forward declaration we need to consider that unique_ptr has own specific behavior with incomplete types. This cases was mentioned here and here.
But when we move definition of forwarded class to another header file and include headers in one place later with usage of client class, compilers become insane - in some special cases of destructor declaration they says about incomplete type.
Here is an minimal example. If uncomment "#define CASE_2" or "#define CASE_3" and try to build it, there will be compilation error.
file foo.h
#ifndef FOO_H
#define FOO_H
class Foo{};
#endif // FOO_H
file base.h
#ifndef BASE_H
#define BASE_H
#include <memory>
//#define CASE_1
//#define CASE_2
//#define CASE_3
class Foo;
class Base
{
public:
#if defined(CASE_1)
~Base() = default; // OK!
#elif defined(CASE_2)
~Base() {}; // error: invalid application of 'sizeof' to incomplete type 'Foo'
#elif defined(CASE_3)
~Base(); // error: invalid application of 'sizeof' to incomplete type 'Foo'
#endif
// OK!
private:
std::unique_ptr<Foo> m_foo;
};
#endif // BASE_H
file base.cpp
#include "base.h"
#if defined(CASE_3)
Base::~Base()
{
}
#endif
file main.cpp
#include "foo.h" // No matter order of this includes
#include "base.h" //
int main()
{
Base b;
}
Upvotes: 3
Views: 1238
Reputation: 44268
But when we move definition of forwarded class to another header file and include headers in one place later with usage of client class, compilers become insane - in some special cases of destructor declaration they says about incomplete type.
Compilers are fine, when destructor of B
is defined definition of class Foo
must be visible too. That happens in CASE_1 for you - destructor defined in main.cpp
and you include foo.h
there. CASE_2 would not compile anyway and should not be used. CASE_3 will compile when you include foo.h
from base.cpp
and you should do that anyway and use this case (and not include foo.h
from main.cpp otherwise you defeat whole purpose of pimpl idiom).
So there is no strange behavior of compilers, your using of pimpl idiom is strange which leads to behavior you observe.
Upvotes: 0
Reputation: 62613
I believe, it has to do with C++ standard 12.4 / 6.
A destructor that is defaulted and not defined as deleted is implicitly defined when it is odr-used (3.2) to destroy an object of its class type (3.7) or when it is explicitly defaulted after its first declaration.
When you have your destructor defaulted, it would only be defined when ODR-used, i.e. when Base
object is destroyed. In your code snippet, no object of such type is ever destroyed, and thus, the program compiles - since deleter of unique_ptr is not actually called anywhere - it is only called by Base
destructor, which is not defined in this scenario.
When you provide a user-defined destructor, it is defined in-place, and program becomes ill-formed, since you can't destruct a unique_ptr
object of incomplete type.
By the way, having destructor declared
, but not defined
(as in ~base();
) does not yield compilation error for the same reason.
Upvotes: 3