Как остановить распространение объявлений через иерархические включения?

#c #c #header-files

#c #c #заголовочные файлы

Вопрос:

Всякий раз, когда я делаю заголовочном файле мне приходит в голову вопрос: «Как остановить распространение объявлений через иерархические включения?»Предположим, что существуют следующие файлы:

foo.h

 #ifndef FOO_H
#define FOO_H

typedef int foo_t;

inline int foo() { return 1; }

class foo_c {};

#endif  /* FOO_H */
  

bar.h

 #ifndef BAR_H
#define BAR_H

#include "Foo.h"

typedef foo_t bar_t;

inline int bar() { return foo(); }

class bar_c : public foo_c {};

#endif  /* BAR_H */
  

zoo.h

 #ifndef ZOO_H
#define ZOO_H

#include "Bar.h"

typedef bar_t zoo_t;

inline int zoo() { return bar(); }

class zoo_c : public bar_c {};

#endif  /* ZOO_H */
  

В файле zoo.h мы можем получить доступ к объявленным элементам foo_c , foo_t foo() и ко всем изменениям в foo.h будет повторноскомпилирован zoo.h

Я знаю, что мы можем перенести реализации в файлы .cpp, но как насчет кодов, написанных в определениях классов в файлах .h? Как мы можем заставить программиста явно включить foo.h в зоопарке.h нужно ли ему это?

В качестве примера в Qt, когда я включаю и использую <QQueue> , у меня нет доступа к тому, QList где QQueue наследуется QList , и я должен включать <QList> явно. (Кроме того, я не знаю, как это делается, и как это влияет на время компиляции)

Комментарии:

1. Ваше использование protect предполагает, что что-то нуждается в защите. Защита от чего ? В чем проблема?

2. @RobKennedy: Прочитайте обновленный вопрос о времени компиляции.

3. Я не уверен, помогает ли это, но у меня есть один трюк для уменьшения зависимостей заголовков. Если вы используете тип только по указателю или ссылке, вы можете просто объявить его перенаправлением вместо включения его заголовка.

4. Если QQueue наследуется от QList, то я обещаю вам, что каждый раз, когда вы используете QQueue, вы также компилируете весь код для QList, независимо от того, включали вы явно qlist.h или нет. Если вы думаете, что я ошибаюсь, опубликуйте код, который демонстрирует проблему, которую вы пытаетесь решить.

5. Обычно вы защищаете себя от изменений заголовка, разделяя объявления и определения. (Предполагается, что объявления функций меняются гораздо реже, чем реализации.) — Если вы реализуете все в заголовках, то вы в основном получаете единый модуль компиляции, содержащий все.

Ответ №1:

В C и C «чтобы остановить распространение объявлений», вам нужно удалить их из общедоступного интерфейса, точка. Переместите их в реализацию. Или на «менее общедоступный» интерфейс.

Время компиляции является одной из целей. Другие — переносимость, сопровождаемость. Также это напрямую связано с слабой связью.

Самый популярный метод C , который может помочь при создании вашего класса, — это идиома Pimpl. Создайте свой класс реализации, включите соответствующий заголовок в cpp реализации и передайте объявление реализации в свой общедоступный интерфейс. Ваши пользователи ничего не будут знать о базовом классе и будут знать только название вашей реализации.

Невозможно остановить распространение, если вы хотите использовать typedef ‘s. Но для обеспечения лучшей переносимости и удобства обслуживания вы можете использовать тот же подход, который эффективно используют библиотеки Boost: тип, определяемый реализацией (например, этот).

Каждый дизайн интерфейса представляет собой компромисс между расширяемостью, сокрытием информации и простотой (или усилием). Если вам нужно заархивировать первые два, используйте более сложный подход. Вы можете предоставить два открытых интерфейса: один для использования, а другой, гораздо более широкий и низкоуровневый, для расширяемости.

Ответ №2:

Я считаю важным четко разделять прямые объявления и определения в моем коде: используйте прямые объявления как можно чаще.

В общем, если вашему классу X не нужно знать размер класса Y, все, что вам нужно, это прямое объявление Y — вам не нужно включать Y.hpp.

Например, если X не является подклассом из Y и X не содержит никаких элементов типа Y, то вам не нужно включать Y.hpp. Достаточно прямого объявления класса Y;. Иногда, чтобы лучше разделить свой код, я буду хранить ссылку или указатель на Y, а не встраивать Y в класс X — если это возможно, опять же, все, что мне нужно сделать, это перенаправить объявление класса Y;

Теперь есть комментарий о невозможности прямого объявления при использовании шаблонных классов. Но в этом есть хитрость — вместо использования typedef, подкласса из экземпляра шаблона, который вы хотите, например:

 class Bars : public std::vector<Bar> { };
  

Теперь вы можете объявлять пересылку class Bars; там, где ранее вы не могли объявлять пересылку std::vector<Bar>;

Итак, это шаги, которым я следую во всех своих проектах на C :

  1. разделите мой код на модули, разделенные пространствами имен
  2. создайте файл fdecl.hpp в каждом модуле, который содержит прямые объявления для этого модуля
  3. настоятельно предпочитаю использовать #include <modulename/fdecl.hpp> вместо любого #include <modulename/foo.hpp> (пересылать объявления вместо определений)

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

Ответ №3:

Я бы переписал код таким образом:

foo.h

 #ifndef FOO_H
#define FOO_H

inline int foo();

#endif  /* FOO_H */
  

foo.cpp

 #include "foo.h"

inline int foo()
{
    return 1;
}
  

bar.h

 #ifndef BAR_H
#define BAR_H

inline int bar();

#endif  /* BAR_H */
  

bar.cpp

 #include "bar.h"
#include "foo.h"

inline int bar()
{
    return foo();
}
  

zoo.h

 #ifndef ZOO_H
#define ZOO_H

inline int zoo();

#endif  /* ZOO_H */
  

zoo.cpp

 #include "zoo.h"
#include "bar.h"

inline int zoo()
{
    // cannot *incidentally* access foo() here, explicit #include "foo.h" needed
    return bar();
}
  

Таким образом, вы предоставляете в заголовке только свой интерфейс, а детали реализации остаются в файле .cpp / .

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

Комментарии:

1. Это не препятствует доступу к чему-либо foo() , но, вероятно, предотвращает его встраивание.

2. Никакого эффекта, вы можете получить доступ к foo() в zoo. h тоже.

3. Хорошо, но подумайте о typedef или каком-то объявлении, которое вам нужно использовать в bar.h

4. @Масуд: в зоопарке. h вы #включаете только bar.h, поэтому у вас нет прямого доступа к foo() (если только вы не включаете foo.h «вручную»).

5. @Mike: вы не можете запретить злонамеренному сотруднику использовать эту функцию. В конце концов, он может использовать LoadLibrary / GetProcAddress в Windows или какой-нибудь подобный низкоуровневый трюк. Что вы можете сделать, так это исключить автоматическое отображение внутренней функции, когда другие включают ваш заголовок.

Ответ №4:

Возможно, вы можете использовать пространства имен:

foo.h

 namespace f {
    inline int foo();
}
  

bar.h

 #include "foo.h"
inline int bar()
{
    using namespace f;
    return foo();
}
  

zoo.h

 #include "bar.h"
inline int zoo()
{
    using namespace b;
    // Cannot use foo here: can only refer to it by the full name f::foo
    return bar();
}
  

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

Обновить:

Тот же принцип может работать с классами и другими именами. Например, с именами Qt:

qt_main.h

 namespace some_obscure_name
{
    class QList {...};
    class QQueue: public QList {...}
    ...
}
  

qt_list.h

 #include "qt_main.h"
using some_obscure_name::QList;
  

qt_queue.h

 #include "qt_main.h"
using some_obscure_name::QQueue;
  

zoo.h:

 #include "qt_queue.h"
...
QQueue myQueue; // OK
QList myList1; // Error - cannot use QList
some_obscure_name::QList myList2; // No error, but discouraged by Qt developers
  

Отказ от ответственности: у меня нет опыта работы с Qt; этот пример не показывает, что на самом деле сделали разработчики Qt, он показывает только то, что они могли сделать.

Ответ №5:

Вы не можете съесть пирог и его тоже. Либо вы максимально используете встроенность, либо максимально ограничиваете видимость. С классами вы должны соблюдать баланс между использованием деривации и / или прямых элементов данных, для которых требуется наличие соответствующего определения класса, или косвенных элементов данных, т. е. указателей или ссылок, для которых требуется только объявление класса. Ваш подход благоприятствует встраиванию / прямому включению, противоположной крайностью было бы:

foo.h

 #ifndef FOO_H
#define FOO_H

typedef int foo_t;

int foo();

class foo_c {};

#endif  /* FOO_H */
  

bar.h

 #ifndef BAR_H
#define BAR_H

typedef foo_t bar_t;

int bar();

class foo_c;

class bar_c {
  public:
    bar_c();
  private:
    foo_c * my_foo_c;
};

#endif  /* BAR_H */
  

zoo.h

 #ifndef ZOO_H
#define ZOO_H

typedef bar_t zoo_t;

int zoo();

class zoo_c {
  public:
    zoo_c();
  private:
    bar_c * my_bar_c;
};

#endif  /* ZOO_H */
  

foo.c

 #include "foo.h"

int foo() {
    return 1;
}
  

bar.c

 #include "bar.h"
#include "foo.h"

int bar() {
    return foo();
}

bar_c::bar_c() : my_foo_c(new foo_c()) {}
  

zoo.c

 #include "zoo.h"
#include "bar.h"

int zoo()
{
    return bar();
}

zoo_c::zoo_c() : my_bar_c(new bar_c()) {}
  

Своего рода промежуточным вариантом было бы ввести дополнительный уровень исходных файлов, который вы могли бы вызывать .inl , переместить туда реализации функций и сделать их встроенными. Таким образом, вы могли бы включить эти новые файлы после исходных заголовков и только там, где это действительно необходимо, и получить как ограниченную видимость, так и максимальную встроенность. Однако я не думаю, что это стоит затраченных усилий.

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

Если вы беспокоитесь о времени компиляции, обычно гораздо проще полагаться на механизм предварительной компиляции заголовка вашего компилятора.

Комментарии:

1. Выбор встроенных функций был моей ошибкой, я должен написать свой вопрос с объявлением класса.