Заголовки С++ - лучшая практика при включении

Заголовки С++

Если у меня есть A.cpp и A.h, а также b.h, c.h, d.h

Я должен делать:

in A.h:

#include "b.h"
#include "c.h"
#include "d.h"

в A.cpp:

#include "A.h"

or

в A.cpp:

#include "A.h"
#include "b.h"
#include "c.h"
#include "d.h"

Есть ли проблемы с производительностью? Очевидные преимущества? Что-то плохое в этом?


person JT.    schedule 09.11.2009    source источник


Ответы (6)


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

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

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

class SomeClass;
// Can now use pointers/references to SomeClass
// without needing the full definition
person Adam Rosenfield    schedule 09.11.2009
comment
@Adam Адам - ​​есть ли у вас пример предварительных объявлений и когда уместно использовать, а когда нет? - person JT.; 09.11.2009
comment
Отличный совет. Я бы также рекомендовал, чтобы файл .c, связанный с .h, всегда включал .h в качестве первого включения. Это делается для того, чтобы все ваши файлы .h всегда могли компилироваться сами по себе (вместо того, чтобы полагаться на порядок включения в вашем файле .c). - person Edan Maor; 09.11.2009
comment
+1 за последнюю часть, раньше я всегда включал заголовочный файл - person Daniel Sloof; 09.11.2009
comment
Вы также можете использовать предварительное объявление, если вы возвращаете SomeClass по значению. - person Frerich Raabe; 10.11.2009
comment
ищущие также ссылаются на ответ @FrerichRaabe ниже. - person ParokshaX; 05.03.2014
comment
Да, сам заголовочный файл должен быть автономным. Он должен содержать все необходимые зависимости. - person Houcheng; 23.08.2017

Ключевой практикой здесь является наличие вокруг каждого файла foo.h защиты, такой как:

#ifndef _FOO_H
#define _FOO_H

...rest of the .h file...
#endif

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

Мне нравится один руководящий принцип, сформулированный Адамом: убедитесь, что, если исходный файл включает только a.h, он не будет неизбежно получать ошибки из-за a.h, предполагая, что другие файлы были включены до него - например. если a.h требует, чтобы b.h был включен раньше, он может и должен просто включить сам b.h (охранники сделают это noop, если b.h был уже включен ранее)

И наоборот, исходный файл должен включать заголовки, из которых он что-то требует (макросы, объявления и т. д.), не предполагать, что другие заголовки появляются волшебным образом, потому что некоторые из них были включены.

Если вы используете классы по значению, увы, вам нужны все детали класса в некоторых .h, которые вы включаете. Но для некоторых применений через ссылки или указатели будет достаточно простого class sic;. Например, при прочих равных условиях, если классу a удается использовать указатель на экземпляр класса b (то есть элемент class b *my_bp;, а не элемент class b *my_b;), связь между включаемыми файлами может быть установлена. слабее (уменьшая количество перекомпиляции) - например. В b.h может быть немного больше, чем class b;, в то время как все кровавые подробности находятся в b_impl.h, который включается только в заголовки, которые действительно в нем нуждаются...

person Alex Martelli    schedule 09.11.2009
comment
Учитывая, что foo.h по-прежнему будет считываться каждый раз, когда он включен, на самом деле это не так. Если бы вы это сделали, это было бы бесполезно: #ifndef _FOO_H #include foo.h #endif - person Bill; 09.11.2009
comment
Да, но это сделало бы каждое включение нечитаемым. Во всех смыслах и целях многократно повторяющееся включение будет найдено в кеше файловой системы, поэтому его повторное включение не повлияет на производительность — и, безусловно, с точки зрения семантики это не будет выполняться, что действительно важно. - person Alex Martelli; 10.11.2009

То, что сказал вам Адам Розенфилд, верно. Пример того, когда вы можете использовать предварительное объявление:

#ifndef A_H_
#define A_H_

    #include "D.h"
    class B;  //forward declaration
    class C;  //forward declaration

    class A
    {
    B *m_pb;  //you can forward declare this one bacause it's a pointer and the compilier doesn't need to know the size of object B at this point in the code.  include B.h in the cpp file.
    C &m_rc;  //you can also forware declare this one for the same reason, except it's a reference.

    D m_d;    //you cannot forward declare this one because the complier need to calc the size of object D.

    };

#endif
person cchampion    schedule 09.11.2009
comment
(Надеюсь, это строится, я не проверял, лол. Дайте мне знать, если у вас возникнут проблемы с этим). - person cchampion; 09.11.2009

Ответ. Пусть A.h включает b.h, c.h и d.h только в том случае, если это необходимо для успешной сборки. Эмпирическое правило: включайте в заголовочный файл столько кода, сколько необходимо. Все, что не требуется немедленно в A.h, должно быть включено в A.cpp.

Обоснование: чем меньше кода включено в заголовочные файлы, тем меньше вероятность того, что вам потребуется перекомпилировать код, использующий заголовочный файл, после внесения каких-либо изменений где-либо. Если между разными заголовочными файлами много #include ссылок, изменение любой из них потребует перестроения всех других файлов, которые включают измененный заголовочный файл - recursivel. Поэтому, если вы решите коснуться какого-либо заголовочного файла верхнего уровня, это может привести к перестройке огромных частей вашего кода.

Используя форвардные объявления везде, где это возможно, в ваших заголовочных файлах, вы уменьшаете связанность исходных файлов и таким образом сделать сборку быстрее. Такие предварительные объявления могут использоваться в гораздо большем количестве ситуаций, чем вы думаете. Как правило, вам нужен заголовочный файл t.h (который определяет тип T), если вы

  1. Объявите переменную-член типа T (обратите внимание, что это не включает объявление указателя на-T).
  2. Напишите несколько встроенных функций, которые обращаются к членам объекта типа T.

Вам не нужно включать объявление T, если ваш заголовочный файл просто

  1. Объявляет конструкторы/функции, которые принимают ссылки или указатели на объект T.
  2. Объявляет функции, которые возвращают объект T по указателю, ссылке или значению.
  3. Объявляет переменные-члены, которые являются ссылками или указателями на объект T.

Рассмотрим это объявление класса; какие из включаемых файлов для A, B и C вам действительно нужно включать?:

class MyClass
{
public:
    MyClass( const A &a );

    void set( const B &b );
    void set( const B *b );
    B getB();
    C getC();

private:
    B *m_b;
    C m_c;
};

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

person Frerich Raabe    schedule 09.11.2009
comment
В вашем примере вы смешиваете A, B и C с X, Y и Z. - person Bill; 09.11.2009
comment
@Bill: Вы правы, я изменил пример, чтобы сказать A, B, C сейчас. - person Frerich Raabe; 10.11.2009
comment
@FrerichRaabe идеальный ответ. +1 за Рассуждения: часть. - person ParokshaX; 05.03.2014

Лучше не включать заголовки в другие заголовки — это замедляет компиляцию и приводит к циклическим ссылкам.

person Martin Beckett    schedule 09.11.2009
comment
@mgd - Если у меня есть заголовок с 10 включениями, а в источнике было 10 разных включений, должен ли я просто переместить включения из заголовка в источник, чтобы в источнике было 20 включений? - person JT.; 09.11.2009
comment
Смотрите более полный ответ Алекса. Единственным исключением из этого является то, что у вас есть предварительно скомпилированные заголовки, в этом случае включите все заголовки платформы/фреймворка/ОС, которые никогда не изменятся в один файл, и включите его везде. - person Martin Beckett; 09.11.2009

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

Я не знаю, есть ли стандартный способ сделать это, но я предпочитаю включать все нужные мне заголовки в исходные файлы, и включать их в заголовки, если это нужно для чего-то в самом заголовке (например, typedef из другой заголовок)

person Jeffrey Aylesworth    schedule 09.11.2009
comment
@Jeff - ах да, это может понадобиться элементам в заголовке. но разве они не получат его, если все заголовки находятся в исходном файле? - person JT.; 09.11.2009