Существует ли стандартное соглашение #include для С++?

Это довольно простой вопрос, но он беспокоил меня некоторое время.

В моем проекте есть куча файлов .cpp (реализация) и .hpp (определение).

Я обнаружил, что, добавляя дополнительные классы и больше взаимозависимостей классов, я должен #include другие заголовочные файлы. Через неделю или две я получаю директивы #include во многих местах. Позже я попытаюсь удалить некоторые из #includes и обнаружу, что все по-прежнему работает, потому что какой-то ДРУГОЙ включенный класс также #включает то, что я только что удалил.

Есть ли простое и легкое правило для добавления #includes, которое в первую очередь предотвратит возникновение этой ужасной неразберихи? Как лучше всего?

Например, я работал над проектами, в которых файл реализации .cpp включает ТОЛЬКО соответствующий файл определения .hpp и ничего больше. Если есть какие-либо другие файлы .hpp, которые должны использоваться реализацией .cpp, на все они ссылается файл определения .hpp.


person Runcible    schedule 27.03.2009    source источник


Ответы (8)


Я всегда использую принцип наименьшей связи. Я включаю файл только в том случае, если он действительно нужен текущему файлу; если я могу обойтись предварительным объявлением вместо полного определения, я буду использовать его. Мои файлы .cpp всегда имеют кучу #includes вверху.

Бар.ч:

class Foo;

class Bar
{
    Foo * m_foo;
};

Бар.cpp:

#include "Foo.h"
#include "Bar.h"
person Mark Ransom    schedule 27.03.2009
comment
Я понимаю. Таким образом, похоже, что ваш файл .h обычно имеет очень мало (если вообще есть) #include. - person Runcible; 27.03.2009
comment
Да - чаще всего это будет для контейнеров стандартной библиотеки, например. #включить ‹вектор›. - person Mark Ransom; 27.03.2009
comment
В одной (умеренно сложной) кодовой базе я заметил заметное улучшение времени перестроения с использованием этого подхода. - person dmckee --- ex-moderator kitten; 27.03.2009
comment
Мне на самом деле нравится этот ответ больше всего, потому что он такой простой. - person Runcible; 31.03.2009
comment
Bar.cpp должен включать Bar.h перед Foo.h, чтобы гарантировать, что Bar.h включает все файлы заголовков, необходимые для компиляции. См. google-styleguide.googlecode.com/svn/trunk/ - person Blair Zajac; 04.05.2011

Некоторые рекомендации:

  • Каждый файл .cpp или .C включает все необходимые заголовки и не зависит от заголовков, включая другие связанные заголовки.
  • Каждый файл .hpp или .h включает все свои зависимости и не зависит от включенных заголовков, включая другие связанные заголовки.
  • Каждый заголовок обернут:

    #ifndef HEADER_XXX_INCLUDED
    #define HEADER_XXX_INCLUDED
    ...
    #endif /* HEADER_XXX_INCLUDED */
    
  • Заголовки не включают друг друга в циклы

  • Часто: существует один «заголовочный файл для всего проекта», например «config.h» или «.h», который всегда включается первым в любой файл .cpp или .C. Обычно это данные конфигурации, связанные с платформой, константы и макросы для всего проекта и т. д.

Это не обязательно «лучшая практика», но правила, которым я обычно следую:

  • Заголовки, относящиеся к проекту, включаются как #include "..." и перед общесистемными заголовками, которые включаются как #include <...>.
  • Заголовки, относящиеся к конкретному проекту, включаются в алфавитном порядке, чтобы гарантировать отсутствие случайного скрытого требования, в каком порядке они будут включены. Поскольку каждый заголовок должен включать в себя его зависимости, и заголовки должны быть защищены от многократного включения, вы должны иметь возможность включать их в любом порядке.
person Antti Huima    schedule 27.03.2009
comment
Я также делаю #endif //HEADER_XXX_INCLUDED, чтобы увидеть, с чем совпадает endif. - person Scott Chamberlain; 27.03.2009
comment
Интересный. Разве это не означает, что файл .cpp будет иметь те же (возможно, больше) #include, что и соответствующий файл .hpp? - person Runcible; 27.03.2009
comment
Меня смущает твоя вторая пуля. ИМХО, вы всегда должны включать заголовки, на которые опирается ваш класс, в файл .h для вашего класса. Если каждый класс будет следовать этой практике, вы сможете положиться на них, чтобы включить все, что им нужно. Вы также выступаете за включение всех их зависимостей? - person rmeador; 27.03.2009
comment
Мне нравится создавать модульные тесты, которые ничего не делают, кроме #include одного заголовочного файла. Непреднамеренные зависимости быстро отсеиваются при компиляции тестов. - person Ferruccio; 27.03.2009
comment
Если вы используете Visual Studio 2005 или более позднюю версию и переносимость не является проблемой, вы можете заменить строки '#ifndef HEADER_XXX' / '#define HEADER_XXX' / '#endif // HEADER_XXX' на простую '#pragma Once' строка, куда будет идти ваша строка #ifndef. - person RobH; 27.03.2009
comment
Вы должны подчеркнуть, что заголовки системы (и других внешних библиотек) должны быть включены в последнюю очередь. Это, вероятно, самый большой источник порядковых включений зависимостей в нашем коде. - person Greg Rogers; 27.03.2009
comment
Чтобы сэкономить время компиляции, я обычно включаю большие системные заголовки, такие как windows.h, поскольку первый включает в каждый файл .cpp и не включаю их в заголовки. Если заголовок зависит от системного заголовка, вы можете выполнить #ifndef для чего-то, что определяет системный заголовок... - person RobH; 27.03.2009
comment
... и поместите строку #error внутри этого блока, которая говорит разработчику включить этот конкретный системный заголовок перед включением вашего заголовка. Строка #error будет выглядеть примерно так: #error Перед включением foobar.h необходимо включить windows.h. - person RobH; 27.03.2009
comment
Общепринятой практикой является всегда включать заголовок, соответствующий файлу cpp, первым в списке заголовков. Это помогает обеспечить правильное включение всех зависимостей заголовка. Может стоит добавить. - person Dan Olson; 28.03.2009
comment
Я не согласен с файлом .cc/.cpp/.C, включая все его заголовочные файлы и прочее, что требуется. Для этого и предназначен соответствующий файл .h. В Foo.cc я #include Foo.h, больше ничего. Foo.h должен содержать все #includes, чтобы убедиться, что мой .cc компилируется и может быть правильно слинкован. - person X-Istence; 28.03.2009
comment
X-Istence, если Foo.cc является реализацией класса Foo, а Foo.h его объявлением, могут быть заголовки, которые вам нужны только для компиляции модуля Foo.cc, но которые не нужны для использования класса. - person Antti Huima; 29.03.2009

Ознакомьтесь с проектом крупномасштабного программного обеспечения C++ Джона Лакоса. Вот чему я следую (написано в качестве примера):

Интерфейс

// foo.h
// 1) standard include guards.  DO NOT prefix with underscores.
#ifndef PROJECT_FOO_H
#define PROJECT_FOO_H

// 2) include all dependencies necessary for compilation
#include <vector>

// 3) prefer forward declaration to #include
class Bar;
class Baz;
#include <iosfwd> // this STL way to forward-declare istream, ostream

class Foo { ... };
#endif

Выполнение

// foo.cxx
// 1) precompiled header, if your build environment supports it
#include "stdafx.h"

// 2) always include your own header file first
#include "foo.h"

// 3) include other project-local dependencies
#include "bar.h"
#include "baz.h"

// 4) include third-party dependencies
#include <mysql.h>
#include <dlfcn.h>
#include <boost/lexical_cast.hpp>
#include <iostream>

Предварительно скомпилированный заголовок

// stdafx.h
// 1) make this easy to disable, for testing
#ifdef USE_PCH

// 2) include all third-party dendencies.  Do not reference any project-local headers.
#include <mysql.h>
#include <dlfcn.h>
#include <boost/lexical_cast.hpp>
#include <iosfwd>
#include <iostream>
#include <vector>
#endif
person Tom    schedule 27.03.2009

Используйте только минимально необходимое количество включений. Бесполезное включение замедляет компиляцию.

Кроме того, вам не нужно включать заголовок, если вам просто нужно указать на класс. В этом случае вы можете просто использовать предварительное объявление, например:

class BogoFactory;

edit: Просто чтобы было ясно. Когда я говорил о минимальной сумме, я не имел в виду построение таких цепочек, как:

a.h
#include "b.h"

b.h
#include "c.h"

Если a.h нуждается в c.h, его, конечно же, необходимо включить в a.h, чтобы предотвратить проблемы с обслуживанием.

person John Smith    schedule 27.03.2009
comment
Как правило, если я не могу обойтись одним или двумя предварительными объявлениями, если файл использует что-то, что определено в заголовке, я включаю этот заголовок, даже если он уже включен в один из заголовков, которые я уже включил. Это предотвращает проблему, на которую указал Джоэл Кохорн. - person RobH; 27.03.2009
comment
Ага, я понял, что мой пост можно так интерпретировать, чего я, конечно, не имел в виду. Отредактировано. - person John Smith; 27.03.2009
comment
Исключением из вышесказанного является случай, когда я включаю системные заголовки. Затем я включу как можно меньше из них. (То есть, если системный заголовок A включает системный заголовок B, а я включаю системный заголовок A, то я не буду также включать системный заголовок B.) - person RobH; 27.03.2009
comment
@RobH: Однако на это не всегда можно положиться. В некоторых системах некоторые системные заголовочные файлы не будут включать то, на что они полагаются, и вам может понадобиться изменить порядок заголовков только для того, чтобы скомпилировать всю эту чертову штуку! Раздражающий! - person X-Istence; 28.03.2009

Есть несколько проблем с моделью #include, используемой в C/C++, главная из которых заключается в том, что она не выражает фактический граф зависимостей. Вместо этого он просто объединяет набор определений в определенном порядке, что часто приводит к тому, что определения идут в другом порядке в каждом исходном файле.

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

Два соглашения (помимо уже упомянутых), которые могут помочь:

  • Один класс == один исходный файл + один заголовочный файл с одинаковыми именами. Класс A идет в A.cpp и A.h. Шаблоны кода и фрагменты кода здесь хороши, чтобы уменьшить количество ввода, необходимого для объявления каждого класса в отдельном файле.
  • Используйте шаблон Impl, чтобы избежать раскрытия внутренних членов в файле заголовка. Шаблон impl означает размещение всех внутренних членов в структуре, определенной в файле .cpp, и наличие только частного указателя с предварительным объявлением в классе. Это означает, что заголовочный файл должен будет включать только те заголовочные файлы, которые необходимы для его открытого интерфейса, а любые определения, необходимые для его внутренних элементов, не будут включены в заголовочный файл.
person JesperE    schedule 27.03.2009

Основываясь на том, что сказал antti.huima:

Допустим, у вас есть классы A, B и C. A зависит (включает) B, а A и B зависят от C. Однажды вы обнаружите, что вам больше не нужно включать C в A, потому что B делает это за вас, и поэтому вы удаляете этот оператор #include.

Что произойдет, если в какой-то момент в будущем вы обновите B, чтобы больше не использовать C? Внезапно A сломался без уважительной причины.

person Joel Coehoorn    schedule 27.03.2009

В A.cpp всегда сначала включайте A.h, чтобы убедиться, что у A.h нет дополнительных зависимостей. Включите все локальные файлы (того же модуля) перед всеми файлами проекта перед всеми системными файлами, опять же, чтобы убедиться, что ничто не зависит от предварительно включенных системных файлов. Максимально используйте предварительные объявления. Используйте шаблон #indef/#define/#endif Если заголовок включен в A.h, вам не нужно включать его в A.cpp. Любые другие заголовки, необходимые A.cpp, должны быть явно включены, даже если они предоставлены другими файлами .h.

person Community    schedule 27.03.2009