Умный указатель с автоматическим созданием
Я ищу простой способ уменьшить связывание заголовков в проекте C++, который происходит главным образом из-за (чрезмерного) состава классов, который, конечно, требует полного типа. Например:
// header A
class A
{
B b; // requires header B
};
Я также рассмотрел интерфейсы и pimpl, но оба подразумевают некоторый шаблонный код, который я не хочу писать / поддерживать вручную (или есть способ сделать это автоматически?).
Поэтому я подумал о замене члена указателем и форвардом class B* pB;
, но это требует обработки создания и удаления объекта. Хорошо, я мог бы использовать умные указатели для удаления (не auto_ptr
хотя, как это требует полного типа при создании, так сказать что-то вроде shared_ptr<class B> pB;
), но как быть с созданием объекта сейчас?
Я мог бы создать объект в A
конструктор, вроде pB = new B;
но это, опять же, руководство, и, что еще хуже, может быть несколько конструкторов... Так что я ищу способ сделать это автоматически, который будет работать так же просто, как изменение B b;
в autoobjptr<class B> pB;
в A
определение без необходимости беспокоиться pB
конкретизации.
Я уверен, что это не новая идея, так что, может быть, вы могли бы дать мне ссылку на общее решение или обсуждение?
ОБНОВЛЕНИЕ: чтобы уточнить, я не пытаюсь сломать зависимость между A
а также B
, но я хочу избежать включения B
Заголовок, когда один включает A
один. На практике, B
используется в реализации A
Таким образом, типичным решением было бы создать интерфейс или pimpl для A
но я ищу что-то более легкое на данный момент.
ОБНОВЛЕНИЕ 2: Я внезапно понял, что ленивый указатель, такой как предложенный здесь, добился бы цели (слишком плохо, что нет стандартной реализации этого, скажем, в boost), в сочетании с виртуальным деструктором (чтобы разрешить неполный тип). Я до сих пор не понимаю, почему не существует стандартного решения, и чувствую, что заново изобретаю колесо...
ОБНОВЛЕНИЕ 3: Внезапно Сергей Таченов пришел с очень простым решением (принятый ответ), хотя мне потребовалось полчаса, чтобы понять, почему это действительно работает... Если вы удалите конструктор A() или определите его встроенным в заголовочном файле, магия больше не работает (ошибка комплимента). Я предполагаю, что когда вы определяете явный не встроенный конструктор, конструирование членов (даже неявных) выполняется внутри того же модуля компиляции (A.cpp), где тип B
завершено. С другой стороны, если ваш A
конструктор встроен, создание членов должно происходить внутри других модулей компиляции и не будет работать как B
там неполно Ну, это логично, но теперь мне любопытно - это поведение определяется стандартом C++?
ОБНОВЛЕНИЕ 4: Надеюсь, финальное обновление. Обратитесь к принятому ответу и комментариям для обсуждения вопроса выше.
5 ответов
Сначала я был заинтригован этим вопросом, так как это выглядело как-то действительно сложно, и все комментарии о шаблонах, зависимостях и включениях имели смысл. Но потом я попытался воплотить это в жизнь и на удивление легко. Так что либо я неправильно понял вопрос, либо вопрос имеет какое-то особенное свойство выглядеть намного сложнее, чем на самом деле. Во всяком случае, вот мой код.
Это прославленный autoptr.h:
#ifndef TESTPQ_AUTOPTR_H
#define TESTPQ_AUTOPTR_H
template<class T> class AutoPtr {
private:
T *p;
public:
AutoPtr() {p = new T();}
~AutoPtr() {delete p;}
T *operator->() {return p;}
};
#endif // TESTPQ_AUTOPTR_H
Выглядит очень просто, и мне стало интересно, работает ли это на самом деле, поэтому я сделал тестовый пример для него. Вот мой бх:
#ifndef TESTPQ_B_H
#define TESTPQ_B_H
class B {
public:
B();
~B();
void doSomething();
};
#endif // TESTPQ_B_H
И b.cpp:
#include <stdio.h>
#include "b.h"
B::B()
{
printf("B::B()\n");
}
B::~B()
{
printf("B::~B()\n");
}
void B::doSomething()
{
printf("B does something!\n");
}
Теперь для класса А, который фактически использует это. Вот ах:
#ifndef TESTPQ_A_H
#define TESTPQ_A_H
#include "autoptr.h"
class B;
class A {
private:
AutoPtr<B> b;
public:
A();
~A();
void doB();
};
#endif // TESTPQ_A_H
И a.cpp:
#include <stdio.h>
#include "a.h"
#include "b.h"
A::A()
{
printf("A::A()\n");
}
A::~A()
{
printf("A::~A()\n");
}
void A::doB()
{
b->doSomething();
}
Хорошо, и, наконец, main.cpp, который использует A, но не включает "bh":
#include "a.h"
int main()
{
A a;
a.doB();
}
Теперь он фактически компилируется без единой ошибки и предупреждения и работает:
d:\alqualos\pr\testpq>g++ -c -W -Wall b.cpp
d:\alqualos\pr\testpq>g++ -c -W -Wall a.cpp
d:\alqualos\pr\testpq>g++ -c -W -Wall main.cpp
d:\alqualos\pr\testpq>g++ -o a a.o b.o main.o
d:\alqualos\pr\testpq>a
B::B()
A::A()
B does something!
A::~A()
B::~B()
Это решает вашу проблему, или я делаю что-то совершенно другое?
РЕДАКТИРОВАТЬ 1: это стандарт или нет?
Ладно, кажется, это было правильно, но теперь это приводит нас к другим интересным вопросам. Вот результат нашего обсуждения в комментариях ниже.
Что происходит в приведенном выше примере? Ах-файлу не нужен bh-файл, потому что он на самом деле ничего не делает с b
, он просто объявляет его и знает его размер, потому что указатель в классе AutoPtr всегда имеет одинаковый размер. Единственные части autoptr.h, которым нужно определение B, - это конструктор и деструктор, но они не используются в ах, так что ах не нужно включать bh
Но почему именно а не использует конструктор Б? Разве поля B не инициализируются всякий раз, когда мы создаем экземпляр A? Если это так, компилятор может попытаться встроить этот код при каждом создании A, но тогда это не удастся. В приведенном выше примере это выглядит как B::B()
вызов помещается в начало скомпилированного конструктора A::A()
в модуле a.cpp, но требует ли стандарт этого?
Сначала кажется, что ничто не мешает компилятору вставлять код инициализации полей всякий раз, когда создается момент, поэтому A a;
превращается в этот псевдокод (не настоящий C++, конечно):
A a;
a.b->B();
a.A();
Могут ли такие компиляторы существовать в соответствии со стандартом? Ответ - нет, они не могли, и стандарт не имеет к этому никакого отношения. Когда компилятор компилирует модуль "main.cpp", он понятия не имеет, что делает конструктор A::A(). Это может быть вызов какого-то специального конструктора для b
, так что вставка по умолчанию, прежде чем он сделает b
инициализируется дважды с разными конструкторами! И компилятор не может проверить это, так как модуль "a.cpp", где A::A()
определяется компилируется отдельно.
Хорошо, теперь вы можете подумать, что если умный компилятор захочет взглянуть на определение B, и если нет другого конструктора, кроме конструктора по умолчанию, тогда он не поставит B::B()
позвонить в A::A()
конструктор и встроить его вместо всякий раз, когда A::A()
называется. Что ж, этого тоже не произойдет, потому что компилятор не может гарантировать, что даже если у B сейчас нет других конструкторов, в будущем его не будет. Предположим, мы добавили это к bh в определении класса B:
B(int b);
Затем мы помещаем его определение в b.cpp и соответственно модифицируем a.cpp:
A::A():
b(17) // magic number
{
printf("A::A()\n");
}
Теперь, когда мы перекомпилируем a.cpp и b.cpp, он будет работать как положено, даже если мы не перекомпилируем main.cpp. Это называется бинарной совместимостью, и компилятор не должен нарушать это. Но если это B::B()
вызов, мы в конечном итоге с main.cpp, который вызывает два B
Конструкторы. Но поскольку добавление конструкторов и не виртуальных методов никогда не должно нарушать бинарную совместимость, любой разумный компилятор не должен допускать этого.
Последняя причина того, что такие компиляторы не существуют, заключается в том, что это не имеет никакого смысла. Даже если инициализация членов является встроенной, это просто увеличит размер кода и не даст абсолютно никакого увеличения производительности, поскольку все еще будет один вызов метода для A::A()
так почему бы не позволить этому методу выполнять всю работу в одном месте?
РЕДАКТИРОВАТЬ 2: Хорошо, как насчет встроенных и автоматически сгенерированных конструкторов A?
Другой вопрос, который возникает, что произойдет, если мы удалим A:A()
от ах и a.cpp? Вот что происходит:
d:\alqualos\pr\testpq>g++ -c -W -Wall a.cpp
d:\alqualos\pr\testpq>g++ -c -W -Wall main.cpp
In file included from a.h:4:0,
from main.cpp:1:
autoptr.h: In constructor 'AutoPtr<T>::AutoPtr() [with T = B]':
a.h:8:9: instantiated from here
autoptr.h:8:16: error: invalid use of incomplete type 'struct B'
a.h:6:7: error: forward declaration of 'struct B'
autoptr.h: In destructor 'AutoPtr<T>::~AutoPtr() [with T = B]':
a.h:8:9: instantiated from here
autoptr.h:9:17: warning: possible problem detected in invocation of delete
operator:
autoptr.h:9:17: warning: invalid use of incomplete type 'struct B'
a.h:6:7: warning: forward declaration of 'struct B'
autoptr.h:9:17: note: neither the destructor nor the class-specific operator
delete will be called, even if they are declared when the class is defined.
Единственное релевантное сообщение об ошибке - "недопустимое использование неполного типа" struct B "". В основном это означает, что main.cpp теперь должен включать bh, но почему? Поскольку автоматически сгенерированный конструктор встроен, когда мы создаем a
в main.cpp. Хорошо, но всегда ли это должно происходить или это зависит от компилятора? Ответ в том, что это не может зависеть от компилятора. Ни один компилятор не может сделать автоматически сгенерированный конструктор не встроенным. Причина в том, что он не знает, куда поместить свой код. С точки зрения программиста ответ очевиден: конструктор должен идти в модуле, где определены все другие методы класса, но компилятор не знает, что это за модуль. Кроме того, методы класса могут быть распределены по нескольким модулям, а иногда это даже имеет смысл (например, если часть класса автоматически генерируется каким-либо инструментом).
И конечно, если мы сделаем A::A()
явно встроенный, либо используя ключевое слово inline, либо поместив его определение в объявление класса A, произойдет та же ошибка компиляции, возможно, немного менее загадочная.
Вывод
Кажется, что это вполне нормально использовать технику, описанную выше для автоматически созданных указателей. Единственное, в чем я не уверен, так это в том, что AutoPtr<B> b;
вещь внутри ах будет работать с любым компилятором. Я имею в виду, что мы можем использовать класс с разделением вперед при объявлении указателей и ссылок, но всегда ли правильно использовать его в качестве параметра создания шаблона? Я думаю, что в этом нет ничего плохого, но компиляторы могут думать иначе. Поиск в Google тоже не дал никаких полезных результатов.
Я уверен, что это может быть реализовано так же, как unique_ptr
реализовано. Разница будет в том, что allocated_unique_ptr
конструктор выделит объект B по умолчанию.
Обратите внимание, однако, что если вы хотите автоматическое построение объекта B, он будет создан с помощью конструктора по умолчанию.
Проблема с вашим подходом состоит в том, что, хотя вы могли бы избежать включения заголовочного файла для B таким способом, он на самом деле не уменьшает зависимости.
Лучший способ уменьшить зависимости - позволить B наследовать от базового класса, объявленного в отдельном заголовочном файле, и использовать указатель на этот базовый класс в A. Вам все равно потребуется вручную создать правильный потомок (B) в конструкторе A, конечно.
Также вполне возможно, что зависимость между A и B реальна, и в этом случае вы ничего не улучшаете, искусственно избегая включения заголовочного файла B.
Ну, вы дали лучшее решение сами, используйте указатели и new
их в конструкторе... Если имеется более одного конструктора, повторите этот код там. Вы можете создать базовый класс, который сделает это за вас, но это только мистифицирует реализацию...
Вы думали о шаблоне в class B
? Это также может решить ваши взаимозависимости заголовков, но, скорее всего, увеличит время компиляции... Что приводит нас к причине, по которой вы пытаетесь избежать этих #include
s. Вы измерили время компиляции? Это беспокоит? Это проблема?
ОБНОВЛЕНИЕ: пример для шаблона:
// A.h
template<class T>
class A
{
public:
A(): p_t( new T ) {}
virtual ~A() { delete p_t }
private:
T* p_t;
};
Опять же, это, скорее всего, не увеличит время компиляции (B.h
нужно будет потянуть, чтобы создать экземпляр шаблона A<B>
), он позволяет вам удалить включения в заголовке A и исходном файле.
Вы могли бы написать автоматический pimpl_ptr<T>
который будет автоматически создавать, удалять и копировать содержимое T.