Предотвращение взрыва заголовка в C++ (или C++0x)

Скажем, с общим кодом, например следующим:

y.hpp:

#ifndef Y_HPP
#define Y_HPP

// LOTS OF FILES INCLUDED

template <class T>
class Y 
{
public:
  T z;
  // LOTS OF STUFF HERE
};

#endif

Теперь мы хотим иметь возможность использовать Y в классе (скажем, X), который мы создаем. Однако мы не хотим, чтобы пользователи X должны были включать заголовки Y.

Итак, мы определяем класс X, что-то вроде этого:

x.hpp:

#ifndef X_HPP
#define X_HPP

template <class T>
class Y;

class X
{
public:
  ~X();
  void some_method(int blah);
private:
  Y<int>* y_;
};

#endif

Обратите внимание: поскольку y_ является указателем, нам не нужно включать его реализацию.

Реализация находится в x.cpp, который компилируется отдельно:

x.cpp:

#include "x.hpp"
#include "y.hpp"

X::~X() { delete y_; }
void X::someMethod(int blah) { y_->z = blah; }

Итак, теперь наши клиенты могут просто включить «x.hpp» для использования X, не включая и не обрабатывая все заголовки «y.hpp»:

main.cpp:

#include "x.hpp"

int main() 
{
  X x;
  x.blah(42);
  return 0; 
}

И теперь мы можем компилировать main.cpp и x.cpp отдельно, и при компиляции main.cpp мне не нужно включать y.hpp.

Однако с этим кодом мне пришлось использовать необработанный указатель, и, кроме того, мне пришлось использовать удаление.

Итак, вот мои вопросы:

(1) Можно ли сделать Y прямым членом (а не указателем на Y) X без необходимости включать заголовки Y? (Я сильно подозреваю, что ответ на этот вопрос отрицательный)

(2) Можно ли использовать класс интеллектуальных указателей для обработки кучи, выделенной Y? unique_ptr кажется очевидным выбором, но когда я меняю строку в x.hpp

от:

Y<int>* y_; 

to:

std::unique_ptr< Y<int> > y_;

и включить и скомпилировать в режиме С++ 0x, я получаю сообщение об ошибке:

/usr/include/c++/4.4/bits/unique_ptr.h:64: error: invalid application of ‘sizeof’ to incomplete type ‘Y<int>’ 
/usr/include/c++/4.4/bits/unique_ptr.h:62: error: static assertion failed: "can't delete pointer to incomplete type"

так можно ли в любом случае сделать это, используя стандартный интеллектуальный указатель вместо необработанного указателя, а также необработанное удаление в пользовательском деструкторе?

Решение:

Говард Хиннант все сделал правильно, все, что нам нужно сделать, это изменить x.hpp и x.cpp следующим образом:

x.hpp:

#ifndef X_HPP
#define X_HPP

#include <memory>

template <class T>
class Y;

class X
{
public:
  X(); // ADD CONSTRUCTOR FOR X();
  ~X();
  void some_method(int blah);
private:
  std::unique_ptr< Y<int> > y_;
};

#endif

x.cpp:

#include "x.hpp"
#include "y.hpp"

X::X() : y_(new Y<int>()) {} // ADD CONSTRUCTOR FOR X();
X::~X() {}
void X::someMethod(int blah) { y_->z = blah; }

И мы можем использовать unique_ptr. Спасибо, Говард!

Обоснование решения:

Люди могут поправить меня, если я ошибаюсь, но проблема с этим кодом заключалась в том, что неявный конструктор по умолчанию пытался инициализировать Y по умолчанию, и поскольку он ничего не знает о Y, он не может этого сделать. Явно говоря, что мы определим конструктор в другом месте, компилятор думает: «Ну, мне не нужно беспокоиться о создании Y, потому что он скомпилирован в другом месте».

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


person Clinton    schedule 28.03.2011    source источник
comment
Следует отметить, что то, что вы описываете, имеет несколько названий, таких как идиома pimpl и Cheshire Cat. См. en.wikipedia.org/wiki/Opaque_pointer для получения дополнительной информации.   -  person Nathan Ernst    schedule 28.03.2011
comment
в качестве примечания, мне кажется, что отказ от многих включений не является достаточной причиной для использования идиомы pimpl. Вы пытаетесь сократить время компиляции? Если да, вы делаете это за счет усложнения вещей (как показывает ваш вопрос), используя кучу вместо стека и т.д.   -  person davka    schedule 29.03.2011


Ответы (3)


Вы можете использовать unique_ptr или shared_ptr для обработки неполного типа. Если вы используете shared_ptr, вы должны наметить ~X(), как вы это сделали. Если вы используете unique_ptr, вы должны указать как ~X(), так и X() (или любой другой конструктор, который вы используете для создания X). Это неявно сгенерированный ctor по умолчанию для X, который требует полного типа Y<int>.

И shared_ptr, и unique_ptr защищают вас от случайного вызова удаления для неполного типа. Это делает их лучше необработанных указателей, которые не обеспечивают такой защиты. Причина, по которой unique_ptr требует описания X(), сводится к тому, что у него есть статический модуль удаления вместо динамического.

Изменить: более глубокое разъяснение

Из-за разницы статического и динамического удаления unique_ptr и shared_ptr два интеллектуальных указателя требуют, чтобы element_type было завершено в разных местах.

unique_ptr<A> требует, чтобы A был завершен для:

  • ~unique_ptr<A>();

Но не для:

  • unique_ptr<A>();
  • unique_ptr<A>(A*);

shared_ptr<A> требует, чтобы A был завершен для:

  • shared_ptr<A>(A*);

Но не для:

  • shared_ptr<A>();
  • ~shared_ptr<A>();

И, наконец, неявно сгенерированный ctor X() будет вызывать как ctor интеллектуального указателя по умолчанию , так и dtor интеллектуального указателя (в случае, если X() выдает исключение, даже если мы знаем, что этого не произойдет).

Итог: любой член X, который вызывает член интеллектуального указателя, где element_type требуется для завершения, должен быть указан в источнике, где element_type завершен.

И самое интересное в unique_ptr и shared_ptr заключается в том, что если вы ошибетесь в том, что нужно выделить, или если вы не поймете, что неявно генерируется специальный элемент, требующий полного element_type, эти интеллектуальные указатели сообщат вам с помощью ( иногда плохо сформулировано) ошибка времени компиляции.

person Howard Hinnant    schedule 28.03.2011

1) Вы правы, ответ "нет": компилятор должен знать размер объекта-члена, и он не может знать его, не имея определения типа Y.

2) boost::shared_ptr (или tr1::shared_ptr) не требует полный тип объекта. Поэтому, если вы можете позволить себе накладные расходы, подразумеваемые этим, это поможет:

Шаблон класса параметризован на T, типе объекта, на который указывает. shared_ptr и большинство его функций-членов не предъявляют требований к T; он может быть неполным или пустым.

Изменить: проверено unique_ptr документов. Кажется, вы можете использовать его вместо этого: просто убедитесь, что ~X() определено там, где строится unique_ptr<>.

person Alexander Poluektov    schedule 28.03.2011
comment
Александр: Почему для unique_ptr это требуется, а для необработанного указателя нет? - person Clinton; 28.03.2011
comment
Проще говоря, потому что unique_ptr должен видеть деструктор Y, когда вызывается деструктор X. - person Alexander Poluektov; 28.03.2011
comment
Александр: Как же Shared_ptr не нуждается в этом? Какой трюк делает shared_ptr, чего не может unique_ptr? - person Clinton; 28.03.2011
comment
shared_ptr запоминает свою функцию удаления (которая по умолчанию является деструктором) во время построения. Чрезвычайно хитрый трюк. Подробнее об этом в конце этой статьи: artima.com/cppsource/top_cpp_aha_moments.html - person Alexander Poluektov; 28.03.2011

Если вам не нравится дополнительный указатель, необходимый для использования идиомы pimpl, попробуйте этот вариант. Во-первых, определите X как абстрактный базовый класс:

// x.hpp, guard #defines elided

class X
{
protected:
    X();

public:
    virtual ~X();

public:
    static X * create();
    virtual void some_method( int blah ) = 0;
};

Обратите внимание, что Y здесь не фигурирует. Затем создайте класс реализации, производный от X:

 #include "Y.hpp"
    #include "X.hpp"

class XImpl 
: public X
{
    friend class X;

private:
    XImpl();

public:
    virtual ~XImpl();

public:
    virtual void some_method( int blah ) = 0;

private:
    boost::scoped_ptr< Y< int > > m_y;
};

X объявил фабричную функцию create(). Реализуйте это, чтобы вернуть XImpl:

// X.cpp

#include "XImpl.h"

X * X::create()
{
    return new XImpl();
}

Пользователи X могут включать X.hpp, который не включает y.hpp. Вы получаете нечто похожее на pimpl, но не имеющее явного дополнительного указателя на объект impl.

person RobH    schedule 28.03.2011