Сначала я был заинтригован этим вопросом, так как это выглядело как-то действительно сложно, и все комментарии по поводу шаблонов, зависимостей и включений имели смысл. Но затем я попытался реализовать это на практике и нашел это на удивление легко. Так что либо я неправильно понял вопрос, либо вопрос имеет особенное свойство выглядеть намного сложнее, чем на самом деле. В любом случае, вот мой код.
Это прославленный 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, но не включает "b.h":
#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: это стандарт или нет?
Хорошо, кажется, это было правильно, но теперь это приводит нас к другим интересным вопросам. Вот результат нашего обсуждения в комментариях ниже.
Что происходит в примере выше? Файл .h не нуждается в файле b.h, потому что он на самом деле ничего не делает с b
, он просто объявляет его и знает его размер, потому что указатель в классе AutoPtr всегда имеет одинаковый размер. Единственные части autoptr.h, которым нужно определение B, - это конструктор и деструктор, но они не используются в a.h, поэтому a.h не обязательно должен включать b.h.
Но почему именно a.h не использует конструктор B? Разве поля 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 сейчас нет других конструкторов, в будущем его не будет. Предположим, мы добавили это в b.h в определении класса 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.h и 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 теперь должен включать b.h, но почему? Поскольку автоматически сгенерированный конструктор встроен, когда мы создаем экземпляр a
в main.cpp. Хорошо, но всегда ли это должно происходить или это зависит от компилятора? Ответ в том, что это не может зависеть от компилятора. Ни один компилятор не может сделать автоматически сгенерированный конструктор не встроенным. Причина в том, что он не знает, куда поместить свой код. С точки зрения программиста, ответ очевиден: конструктор должен идти в модуле, где определены все другие методы класса, но компилятор не знает, что это за модуль. Кроме того, методы класса могут быть распределены по нескольким модулям, а иногда это даже имеет смысл (например, если часть класса автоматически генерируется каким-либо инструментом).
И, конечно, если мы сделаем A::A()
явно встроенным либо используя ключевое слово inline, либо поместив его определение в объявление класса A, произойдет та же ошибка компиляции, возможно, немного менее загадочная.
Заключение
Кажется, что вполне нормально использовать описанную выше технику для автоматически создаваемых указателей. Единственное, в чем я не уверен, это то, что AutoPtr<B> b;
вещь внутри a.h будет работать с любым компилятором. Я имею в виду, что мы можем использовать класс с разделением вперед при объявлении указателей и ссылок, но всегда ли правильно использовать его в качестве параметра создания шаблона? Я думаю, что в этом нет ничего плохого, но компиляторы могут думать иначе. Поиск в Google тоже не дал никаких полезных результатов.