Деструктор не по умолчанию вызывает ошибку неполного типа

В этом примере показано странное поведение компиляторов (msvc14, gcc, clang), но объяснения я не нашел.

Когда мы реализуем идиому pipml и используем предварительное объявление, нам нужно учитывать, что unique_ptr имеет собственное специфическое поведение с неполными типами. Эти случаи упоминались здесь и здесь.

Но когда мы переносим определение пересылаемого класса в другой заголовочный файл и включаем заголовки в одном месте позже с использованием клиентского класса, компиляторы сходят с ума - в некоторых частных случаях объявления деструктора они говорят о неполном типе.

Вот минимальный пример. Если раскомментировать "#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;
}

person degreeme    schedule 26.07.2017    source источник
comment
не уверен, но я думаю, что компилятор на самом деле жалуется не на деструктор, а на конструктор по умолчанию, который отсутствует, если вы объявляете деструктор не по умолчанию   -  person 463035818_is_not_a_number    schedule 26.07.2017
comment
CASE_3 прекрасно компилируется, если вы включаете foo.h из base.cpp, что вы и так предполагаете делать. И CASE_1 не компилируется, если вы не включаете foo.h из main.cpp, чего делать не следует.   -  person Slava    schedule 26.07.2017


Ответы (2)


Я считаю, что это связано со стандартом С++ 12.4/6.

Деструктор, который установлен по умолчанию и не определен как удаленный, определяется неявно, когда он используется odr (3.2) для уничтожения объекта своего типа класса (3.7) или когда он явно устанавливается по умолчанию после его первого объявления.

Когда у вас есть деструктор по умолчанию, он будет определен только при использовании ODR, т. е. при уничтожении Base объекта. В вашем фрагменте кода ни один объект такого типа никогда не уничтожается, и, таким образом, программа компилируется - поскольку средство удаления unique_ptr фактически нигде не вызывается - оно вызывается только деструктором Base, который не определен в этом сценарии.

Когда вы предоставляете определяемый пользователем деструктор, он определяется на месте, и программа становится неправильно сформированной, поскольку вы не можете уничтожить объект unique_ptr незавершенного типа.

Кстати, наличие деструктора declared, а не defined (как в ~base();) не дает ошибки компиляции по той же причине.

person SergeyA    schedule 26.07.2017
comment
Я не уверен, что ни один объект такого типа никогда не уничтожался, у нас есть Base b; в main.cpp, и его следует уничтожить, когда он выходит за рамки. Наверное. - person degreeme; 26.07.2017
comment
@grademe, да, но ПОСЛЕ вы включили определение Foo. Нет в заголовке Base. - person SergeyA; 26.07.2017

Но когда мы переносим определение пересылаемого класса в другой заголовочный файл и включаем заголовки в одном месте позже с использованием клиентского класса, компиляторы сходят с ума - в некоторых частных случаях объявления деструктора они говорят о неполном типе.

Компиляторы в порядке, когда определен деструктор B, определение класса Foo тоже должно быть видимым. Это происходит в CASE_1 для вас - деструктор определен в main.cpp, и вы включаете туда foo.h. CASE_2 в любом случае не будет компилироваться и не должен использоваться. CASE_3 будет скомпилирован, когда вы включите foo.h из base.cpp, и вы должны сделать это в любом случае и использовать этот случай (и не включать foo.h из main.cpp, иначе вы потеряете всю цель идиомы pimpl).

Таким образом, нет странного поведения компиляторов, ваше использование идиомы pimpl странно, что приводит к поведению, которое вы наблюдаете.

person Slava    schedule 26.07.2017