В описанной ситуации имеется один вполне определенный порядок, при котором все работает так, как должно.
Давайте теперь рассмотрим ситуацию, когда фрагменты 1, 2, 3 и 4 находятся в четырех различных заголовочных файлах
s1.h
,
s2.h
,
s3.h
и
s4.h
. Теперь все становится существенно хуже: семантика
B::g
зависит от порядка включения заголовочных файлов не только в
s4.h
, но и в любой код, который включает
s4.h
!
В частности, файл реализации
client_code.срр
может пытаться включить заголовочные файлы в любом порядке:
• если
s3.h
идет перед
s2.h
, то
B::g
будет вызывать
A::f(int)
;
• иначе если
s1.h
идет перед
s2.h
, то
B::g
будет вызывать
A::f(doublе)
;
• иначе
B::g
не будет компилироваться вовсе.
Ситуация стала хуже потому, что два файла реализации могут включать заголовочные файлы в разном порядке. Что произойдет, если
client_code_1.срр
включает
s1.h
,
s2.h
и
s4.h
в указанном порядке, a
client_code_2.срр
включает в соответствующем порядке
s3.h
,
s2.h
и
s4.h
? Тогда
B::g
нарушает правило одного определения (one definition rule — ODR), поскольку имеются две несогласующиеся несовместимые реализации, которые не могут быть верными одновременно: одна из них пытается вызвать
A::f(int)
, а вторая —
A::f(doublе)
.
Поэтому никогда не используйте директивы и объявления
using
для пространств имен в заголовочных файлах либо перед директивой
#include
в файле реализации. В случае нарушения этого правила вы несете ответственность за возможное изменение смысла следующего за
using
кода, например, вследствие загрязнения пространства имен или неполного списка импортируемых имен. (Обратите внимание на "директивы и объявления
using
для пространств имен". Указанное правило неприменимо при описании члена класса с помощью объявления
using
для внесения, при необходимости, имен из базового класса.)
Во всех заголовочных файлах, как и в файлах реализации до последней директивы
#include
, всегда используйте явные полностью квалифицированные имена. В файлах реализации после всех директив
#include
вы можете и должны свободно использовать директивы и объявления
using
. Это верный способ сочетания краткости кода с модульностью.
Исключения
Перенесение большого проекта со старой до-ANSI/ISO реализации стандартной библиотеки (все имена которой находятся в глобальном пространстве имен) к использованию новой (где практически все имена находятся в пространстве имен
std
) может заставить вас аккуратно разместить директиву
using
в заголовочном файле. Этот способ описан в [Sutter02].
Ссылки
[Stroustrup00] §9.2.1 • [Sutter02] §39-40
60. Избегайте выделения и освобождения памяти в разных модулях
Резюме
Золотое правило программиста — положи, где взял. Выделение памяти в одном модуле, а освобождение в другом делает программу более хрупкой, создавая тонкую дальнюю зависимость между этими модулями. Такие модули должны быть компилируемы одной и той же версией компилятора с одними и теми же флагами (в частности, отладочные версии и версии
NDEBUG
) и с одной и той же реализацией стандартной библиотеки; кроме того, с практической точки зрения лучше, чтобы модуль, выделяющий память, оставался загружен при ее освобождении.
Обсуждение
Разработчики библиотек хотят улучшить
их качество, и, как прямое следствие, внутренние структуры данных и алгоритмы, используемые стандартными распределителями памяти, могут существенно различаться в разных версиях. Более того, к значительным изменениям во внутренней работе распределителей памяти могут приводить даже различные опции компилятора (например, включение или отключение отладочных возможностей).
Следовательно, о функции освобождения памяти (т.е. операторе
::operator delete
или функции
std::free
) при пересечении границ модулей практически нельзя строить какие-либо предположения, в особенности при пересечении границ модулей, при котором вы не можете гарантировать, что они будут скомпилированы одним и тем же компилятором С++ с одними и теми же опциями. Конечно, часто эти модули находятся в одном и том же файле проекта и компилируются с одними и теми же опциями, но комфорт часто приводит к забывчивости. В особенности высока цена такой забывчивости при переходе к динамически связываемым библиотекам, распределении большого проекта между несколькими группами или при замене модулей "на ходу" — в этом случае вы должны уделить максимум внимания тому, чтобы выделение и освобождение памяти выполнялось в пределах одного модуля или подсистемы.
Хорошим методом обеспечения освобождения памяти соответствующей функцией является использование
shared_ptr
(см. [C++TR104]). Интеллектуальный указатель
shared_ptr
со счетчиком ссылок может захватить свой "удалитель" в процессе конструирования. "Удалитель" — это функциональный объект (или обычный указатель на функцию), который выполняет освобождение памяти. Поскольку упомянутый функциональный объект, или указатель на функцию, является частью состояния объекта
shared_ptr
, модуль, выделивший память объекту, может одновременно определить функцию освобождения памяти, и эта функция будет корректно вызвана, даже если точка освобождения находится где-то в другом модуле — вероятно, относительно небольшой ценой (корректность важнее цены; см. также рекомендации 5, 6 и 8). Конечно, исходный модуль при этом должен оставаться загруженным.
Ссылки
[С++TR104]
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 определяйте в заголовочном файле и статические объекты уровня пространства имен, например: