Герб Саттер - Стандарты программирования на С++. 101 правило и рекомендация
Большинство программистов интуитивно понимают, почему директива using (например, using namespace A;) вызывает загрязнение в случае воздействия на код, следующий за ней и не осведомленный о наличии этой директивы: поскольку эта директива полностью импортирует одно пространство имен в другое, включая даже те имена, которые до сих пор не были видны, понятно, что это может легко изменить смысл следующего за директивой кода.
Но вот одна распространенная ошибка: многие считают, что использование объявления using на уровне пространства имен (например, using N::Widget;) вполне безопасно. Однако это не так. Такие объявления, как минимум, опасны, причем более тонким и хитрым способом. Рассмотрим следующий код:
// Фрагмент 1
namespace A {
int f(double);
}
// Фрагмент 2
namespace B {
using A::f;
void g();
}
// Фрагмент 3
namespace A {
int f(int);
}
// Фрагмент 4
void В::g() {
f(1); // какая перегрузка будет вызвана?
}
Здесь опасность заключается в том, что объявление using использует текущий список имен f в пространстве имен А в тот момент, когда это объявление встречается. Таким образом, какая именно перегрузка будет видима в пространстве имен В, зависит от того, где именно находится приведенный код фрагментов и в каком порядке он скомбинирован. (Здесь должен раздаться предупреждающий рев вашей внутренней сирены — "Зависимость от порядка есть зло!") Вторая перегрузка, f(int), в большей степени соответствует вызову f(1), но f(int) будет невидима для B::g, если ее объявление окажется после объявления using.
Рассмотрим два частных случая. Пусть фрагменты 1, 2 и 3 находятся в трех различных заголовочных файлах s1.h, s2.h и s3.h, а фрагмент 4 — в файле реализации s4.срр, который включает указанные заголовочные файлы. Тогда семантика B::g зависит от порядка, в котором заголовочные файлы включены в s4.срр! В частности:
• если s3.h идет перед s2.h, то B::g будет вызывать A::f(int);
• иначе если s1.h идет перед s2.h, то B::g будет вызывать A::f(doublе);
• иначе B::g не будет компилироваться вовсе.
В описанной ситуации имеется один вполне определенный порядок, при котором все работает так, как должно.
Давайте теперь рассмотрим ситуацию, когда фрагменты 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, и когда приходит время сборки (компоновки, или связывания), компоновщик сталкивается с наличием нескольких объектов, которые носят одно и то же имя и борются между собой за видимость. Решение проблемы простое — в заголовочный файл надо поместить только объявления: