61. Не определяйте в заголовочном файле объекты со связыванием
61. Не определяйте в заголовочном файле объекты со связыванием
Резюме
Объекты со связыванием, включая переменные или функции уровня пространства имен, обладают выделенной для них памятью. Определение таких объектов в заголовочных файлах приводит либо к ошибкам времени компоновки, либо к бесполезному расходу памяти. Помещайте все объекты со связыванием в файлы реализации.
Обсуждение
Когда мы начинаем использовать С++, то все достаточно быстро уясняем, что заголовочный файл наподобие
// Избегайте определения объектов с внешним
// связыванием в заголовочном файле
int fudgeFactor;
string hello("hello, world!");
void foo() { /* ... */ }
будучи включен больше чем в один исходный файл, ведет при компиляции к ошибкам дублирования символов во время компоновки. Причина проста: каждый исходный файл в действительности определяет и выделяет пространство для fudgeFactor, hello и тела foo, и когда приходит время сборки (компоновки, или связывания), компоновщик сталкивается с наличием нескольких объектов, которые носят одно и то же имя и борются между собой за видимость. Решение проблемы простое — в заголовочный файл надо поместить только объявления:
extern int fudgeFactor;
extern string hello;
void foo(); // В случае объявления функции "extern"
// является необязательным
Реальные же объявления располагаются в одном файле реализации:
int fudgeFactor;
string hello("hello, world!");
void foo() { /* ... */ }
He определяйте в заголовочном файле и статические объекты уровня пространства имен, например:
// избегайте определения объектов со статическим
// связыванием в заголовочном файле
static int fudgeFactor;
static string hello("Hello, world!");
static void foo() { /* ... */ }
Такое некорректное использование ключевого слова static более опасно, чем простое определение глобальных объектов в заголовочном файле. В случае глобальных объектов, по крайней мере, компоновщик в состоянии обнаружить наличие дублей. Но статические данные и функции могут дублироваться на законных основаниях, поскольку компилятор делает закрытую копию для каждого исходного файла. Так что если вы определите статические данные и статические функции в заголовочном файле и включите его в 50 файлов, то тела функций и пространство для данных в выходном исполняемом файле будут дублированы 50 раз (только если у вас не будет использован очень интеллектуальный компоновщик, который сможет распознать 50 одинаковых тел функций и наличие одинаковых константных данных, которые можно безопасно объединить). Излишне говорить, что глобальные данные (такие как статические fudgeFactor) на самом деле не являются глобальными объектами, поскольку каждый исходный файл работает со своей копией таких данных, независимой от всех остальных копий в программе.
Не пытайтесь обойти эту ситуацию при помощи использования безымянных пространств имен в заголовочных файлах, поскольку результат будет ничуть не лучше:
// В заголовочном файле это приводит к тому же
// эффекту, что и использование static
namespace {
int fudgeFactor;
string hello("Hello, world!");
void foo() { /* ... */ }
}
Исключения
В заголовочных файлах могут находиться следующие объекты с внешним связыванием.
• Встраиваемые функции. Они имеют внешнее связывание, но компоновщик гарантированно не отвергает многократные копии. Во всем остальном они ведут себя так же, как и обычные функции. В частности, адрес встраиваемой функции гарантированно будет единственным в пределах программы.
• Шаблоны функций. Аналогично встраиваемым функциям, инстанцирования ведут себя так же, как и обычные функции, с тем отличием, что их дубликаты приемлемы (и должны быть идентичны). Само собой разумеется, хороший компилятор устранит излишние копии.
• Статические данные-члены шаблонов классов. Они могут оказаться особенно сложными для компоновщика, но это уже не ваша проблема — вы просто определяете их в своем заголовочном файле и предоставляете сделать все остальное компилятору и компоновщику.
Кроме того, методика инициализации глобальных данных, известная как "Счетчики Шварца" ("Schwarz counters"), предписывает использование в заголовочном файле статических данных (или безымянных пространств имен). Джерри Шварц (Jerry Schwarz) использовал эту методику для инициализации стандартных потоков ввода-вывода cin, cout, cerr и clog, что и сделало ее достаточно популярной.
Ссылки
[Dewhurst03] §55 • [Ellis90] §3.3 • [Stroustrup00] §9.2, §9.4.1