умный указатель с автоматическим созданием - PullRequest
4 голосов
/ 15 декабря 2010

Я ищу простой способ уменьшить связывание заголовков в проекте 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, но сейчас я ищу что-то более простое.

UPDATE2: Я внезапно осознал, что ленивый указатель, такой как предложенный здесь , сработает (очень плохо, что стандартная реализация этого, скажем, boost), в сочетании с виртуальный деструктор (для разрешения неполного типа). Я до сих пор не понимаю, почему нет стандартного решения, и чувствую, что заново изобретаю колесо ...

ОБНОВЛЕНИЕ3: Внезапно Сергей Таченов пришел с очень простым решением (принятый ответ), хотя мне потребовалось полчаса, чтобы понять, почему это действительно работает ... Если вы удалите конструктор A () или определите его встроенным в заголовочном файле, магия больше не будет работать (ошибка компиляции). Я предполагаю, что когда вы определяете явный не встроенный конструктор, конструирование элементов (даже неявных) выполняется внутри того же модуля компиляции (A.cpp), где завершен тип B. С другой стороны, если ваш конструктор A является встроенным, создание элементов должно происходить внутри других модулей компиляции и не будет работать, так как B там неполон. Ну, это логично, но теперь мне интересно, это поведение определяется стандартом C ++?

UPDATE4: Надеюсь, финальное обновление. Обратитесь к принятому ответу и комментариям для обсуждения вопроса выше.

Ответы [ 5 ]

3 голосов
/ 15 декабря 2010

Сначала я был заинтригован этим вопросом, так как это выглядело как-то действительно сложно, и все комментарии по поводу шаблонов, зависимостей и включений имели смысл. Но затем я попытался реализовать это на практике и нашел это на удивление легко. Так что либо я неправильно понял вопрос, либо вопрос имеет особенное свойство выглядеть намного сложнее, чем на самом деле. В любом случае, вот мой код.

Это прославленный 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 тоже не дал никаких полезных результатов.

1 голос
/ 15 декабря 2010

Я почти уверен, что это может быть реализовано так же, как реализовано unique_ptr. Разница будет в том, что конструктор allocated_unique_ptr будет выделять объект B по умолчанию.

Обратите внимание, однако, что если вы хотите автоматическое построение объекта B, для него будет реализован конструктор по умолчанию .

0 голосов
/ 15 декабря 2010

Вы можете написать автоматический pimpl_ptr<T>, который будет автоматически создавать, удалять и копировать содержащиеся в нем T.

0 голосов
/ 15 декабря 2010

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

Лучший способ уменьшить зависимости - позволить B получить избазовый класс, объявленный в отдельном заголовочном файле, и использующий указатель на этот базовый класс в A. Конечно, вам все равно придется вручную создать правильного потомка (B) в конструкторе A.

Этотакже вполне возможно, что зависимость между A и B является реальной, и в этом случае вы ничего не улучшаете, искусственно избегая включения заголовочного файла B.

0 голосов
/ 15 декабря 2010

Ну, вы сами дали лучшее решение, использовали указатели и 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 и исходного файла.

...