不是默认析构函数导致不完整的类型错误

Not default destructor causes incomplete type error

此示例显示了编译器(msvc14、gcc、clang)的奇怪行为,但我没有找到解释。

当我们实现 pipml 习惯用法并使用前向声明时,我们需要考虑 unique_ptr 具有不完整类型的特定行为。 此案例被提及 here and here

但是当我们将 forwarded class 的定义移动到另一个头文件并稍后使用客户端 class 在一个地方包含头文件时,编译器变得疯狂 - 在析构函数声明的某些特殊情况下它们说到不完整的类型。

这是一个最小的例子。如果取消注释“#define CASE_2”或“#define CASE_3”并尝试构建它,将会出现编译错误。

文件foo.h

#ifndef FOO_H
#define FOO_H

class Foo{};

#endif // FOO_H

文件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

文件base.cpp

#include "base.h"

#if defined(CASE_3)
Base::~Base()
{
}
#endif

文件main.cpp

#include "foo.h"  // No matter order of this includes
#include "base.h" //

int main()
{
    Base b;
}

我相信,这与 C++ 标准 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.

当您的析构函数默认时,它只会在使用 ODR 时被 定义,即当 Base 对象被销毁时。在你的代码片段中,没有这种类型的对象被销毁,因此,程序编译 - 因为 unique_ptr 的删除器实际上没有在任何地方调用 - 它只被 Base 析构函数调用,它不是在此场景中定义。

当您提供用户定义的析构函数时,它是就地定义的,并且程序变得不正确,因为您无法析构 unique_ptr 不完整类型的对象。

顺便说一句,有析构函数 declared,但没有 defined(如 ~base();)不会因为同样的原因产生编译错误。

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.

编译器很好,当 B 的析构函数被定义时 class Foo 的定义也必须可见。这发生在 CASE_1 中 - 在 main.cpp 中定义的析构函数,你在其中包含 foo.h。 CASE_2 无论如何都不会编译,不应使用。 CASE_3 将在您从 base.cpp 中包含 foo.h 时进行编译,无论如何您都应该这样做并使用这种情况(而不是从 main.cpp 中包含 foo.h 否则您将失败pimpl 成语的目的)。

所以编译器没有奇怪的行为,你对 pimpl 习语的使用很奇怪,这导致了你观察到的行为。