Как прекратить распространение объявлений через иерархические включения? - PullRequest
2 голосов
/ 19 октября 2011

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

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 приведет к re -компиляция zoo.h

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

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

Ответы [ 5 ]

3 голосов
/ 24 октября 2011

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

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

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

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

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

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

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

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

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

3 голосов
/ 23 октября 2011

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

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

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

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

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

2 голосов
/ 19 октября 2011

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

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

2 голосов
/ 19 октября 2011

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

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 голос
/ 19 октября 2011

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

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, переместите реализации функций туда и сделайте их встроенными.Таким образом, вы можете включить эти новые файлы после оригинальных заголовков и только там, где это действительно необходимо, и получить как ограниченную видимость, так и максимальную вставку.Однако я не думаю, что это стоит затраченных усилий.

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

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

...