Как защитить хедер от повторного включения

Как происходит компиляция

Напомним, что на прошлой практике был создан проект, состоящий из трёх файлов:

  • 1.cpp — main (вызывает foo() )
  • 2.cpp — foo — определение
  • 2.h — foo — объявление

Заметим, что этот же код можно было написать, без использования 2.h .

Возникает законный вопрос: зачем был создан файл 2.h ?

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

Защита от повторного включения заголовочных файлов

Иногда может возникнуть ситуация, когда один заголовочный файл включён не один, а несколько раз. Посмотрим, к каким проблемам это может привести. Если в файле содержались только объявления (как в файле 2.h из примера), то ничего страшного не произойдёт (стол останется столом, сколько об этом не объявляй), но не всегда всё так удачно. Действительно, ведь заголовочный файл кроме объявлений файлов может содержать и определения (например констант или классов). Тогда его повторное включение приведет к ошибке компоновки. Чтобы этого избежать, все заголовочные файлы следует защищать от повторного включения.

Делается это так:

Здесь директива #ifndef указывает препроцессору, что участок кода до #endif следует компилировать только в случае, если объявления _2_H_ не было. Директива #define же указывает, что _2_H_ следует объявить.

Допустим, что есть файл 3.cpp .

Что произойдет при препроцессинге этого файла?

Вместо каждого #include «2.h» будет подставлено содержимое соответствующего файла. На момент первой подстановки _2_H_ еще не определено, по этому произойдут подстановка объявления функции и объявление _2_H_ . На момент же, когда препроцессор перейдет ко второму включению, _2_H_ уже определено, и потому подстановка выполнена не будет.

Так как для совершение столь распространенного действия приходится писать целых три директивы, а к тому же следить за уникальностью объявляемых констант — была придумана директива #pragma once , которая, будучи помещенной в начало заголовочного файла , позволяет добиться того же результата. Однако пользоваться ей надо осторожно, так как в стандарт она не вошла, и потому поддерживается не всеми компиляторами (gcc и компилятор компании Microsoft — поддерживают).

Подробнее о препроцессоре

Препроцессор — весьма мощное средство, и в языке C он использовался весьма широко.

Возьмем для примера функцию, вычисляющую максимум:

В языке C нет перегрузки функций.

Поэтому, при необходимости написать функцию максимума, принимающая аргументы как целого, так и действительного типов приходится использовать препроцессор:

  • Происходит текстовая подстановка в макрос. (Результат работы программы будет 2, а не 1)
  • В случае если не все параметры заключены в скобки, возможны неожиданности с приоритетами операций.

Сборка

Вспомним, как происходит процесс сборки:

Из этой картинки хорошо видно, зачем нужен Makefile . Процесс сборки весьма громоздок, причем большая часть процесса проделывается для каждого файла независимо от других. Значит, если проделывать каждый шаг лишь для тех файлов, для которых это необходимо (зависимости которых обновились) сборка значительно ускорится. (а для больших проектов, например OpenOffice, она может занимать несколько часов)

Многие компиляторы умеют сами обнаруживать зависимости и создавать Makefile .

Библиотеки

Какие бывают библиотеки?

  • Статические ( *.a , *.lib ) — код функций вставляется в исполняемый файл
  • Динамические ( *.so , *.dll ) — в исполняемый файл вставляется имя функции и ее адрес в библиотеке. Для работы необходим файл библиотеки

Отличие библиотек от программ — нет точки входа int main() .

Пример подключения библиотек:

g++ это синоним для gcc -lstd++ , что указывает, что нужно линковать со стандартной библиотекой с++. По умолчанию приложение собирается с динамическими библиотеками, в случае, если требуется собрать со статическими необходимо явно указать ключ -static . Кстати, как уже говорилось, gcc — это огромный конвейер, содержащий несколько различных компиляторов, и вызов g++ ничего не говорит, о языке написанной нами программы. Информацию о том, какой компилятор следует использовать gcc получает из расширения файла.

Упражнение 1. Попробовать собрать статическую/динамическую библиотеку и использовать ее в программе.

Подробнее об объектных файлах

В объектных файлах не содержится информации о языке, на котором была написана программа (их формат одинаков для различных языков, различен для разных платформ). По этому можно использовать библиотеки, написанные на языке отличном от языка программы. В каждом объектном файле написана написана информация, об объявленных функциях, переменных — она необходима линковщику. Посмотреть эту информацию, можно, например с помощью программы objdump .

Упражнение 2. Посмотреть содержимое объектного файла.

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

Почему компилятор переименовывает функции?

В языке C++ возможна перегрузка функций, т.е. есть возможность создать несколько функций с одинаковыми названиями, но разными типами принимаемых и возвращаемых значений.

Так как в объектном файле информации о языке уже не сохраняется (в том числе о типах передаваемых/возвращаемых значений), то для того, чтобы эти функции различать (иначе бы при линковке возникла ошибка повторного определения) компилятор дописывает к названиям функций информацию о типах (этот процесс называется маскированием). Восстановить объявление функции по ее расширенному названию можно, например, с помощью программы c++filt.

Как уже говорилось, названия функций можно восстановить из объектного файла, а часто эта информация совершенно излишня. В случаях, когда размер исполняемой программы или библиотеки критичен, его можно уменьшить, заменив названия функций на более короткие. Одна из программ, разработанных специально для этого случая: strip.

Header Guard в C ++

Header Guards в C ++ — это директивы условной компиляции, которые помогают избежать ошибок, которые возникают, когда одна и та же функция или переменная определяется более одного раза по ошибке программиста. Согласно C ++, когда функция или переменная определяется более одного раза, возникает ошибка. Ниже приведена программа, иллюстрирующая то же самое:

Программа 1:

Выход:

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

Программа 2:

Выход:

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

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

В дальнейшем возникает фатальная ошибка, которая иногда становится неразрешимой. Для этого в C ++ есть некоторые директивы препроцессора, которые позволяют избежать этой ошибки.

Стражи включения заголовочных файлов

Любой заголовочный файл C/C++ должен иметь следующую структуру:

/* здесь помещается остальной текст заголовочного файла */

Например, заголовочный файл myFunctions.h, в котором размещены объявления функций f и g , будет выглядеть так:

double g(double a, double b);

Директивы ifndef-define-endif, которые обрамляют любой грамотно оформленный заголовочный файл, являются трюком препроцессора: они обеспечивают то, что любой заголовочный файл будет включён в любой исходный файл не более одного раза.

Каждому заголовочному файлу «вручную» ставится в соответствие некоторый «символ», обычно связанный с именем этого файла, чтобы обеспечить уникальность. В первой строке проверяется, был ли уже определён этот символ ранее, если да, то весь остальной текст игнорируется. Если нет, то этот символ определяется, а затем вставляется и весь остальной текст заголовочного файла. Последняя строка ( endif ) просто означает закрытие такого «условного оператора».

На самом деле, если написать подряд две одинаковые директивы include для одного заголовочного файла, содержащего только объявления функций, то ничего страшного не произойдёт, даже если он не содержит стражей включения. Однако для более сложных элементов заголовочных файлов, таких как объявление enum или struct , попадание нескольких их экземпляров в один исходный файл может привести к ошибкам компиляции. Поскольку заголовочные файлы имеют тенденцию быстро изменяться и расти, считается необходимым всегда использовать стражи включения.

Руководство Google по стилю в C++. Часть 2

Часть 1. Вступление
Часть 2. Заголовочные файлы
Часть 3. Область видимости

Все мы при написании кода пользуемся правилами оформления кода. Иногда изобретаются свои правила, в других случаях используются готовые стайлгайды. Хотя все C++ программисты читают на английском легче, чем на родном, приятнее иметь руководство на последнем.
Эта статья является переводом части руководства Google по стилю в C++ на русский язык.
Исходная статья (fork на github), обновляемый перевод.

Заголовочные файлы

Желательно, чтобы каждый .cc файл исходного кода имел парный .h заголовочный файл. Также есть известные исключения из этого правила, такие как юниттесты или небольшие .cc файлы, содержащие только функцию main().

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

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

Независимые заголовочные файлы

Заголовочные файлы должны быть самодостаточными (в плане компиляции) и иметь расширение .h. Другие файлы (не заголовочные), предназначенные для включения в код, должны быть с расширением .inc и использоваться в паре с включающим кодом.

Все заголовочные файлы должны быть самодостаточыми. Пользователи и инструменты разработки не должны зависеть от специальных зависимостей при использовании заголовочного файла. Заголовочный файл должен иметь блокировку от повторного включения и включать все необходимые файлы.

Предпочтительно размещать определения для шаблонов и inline-функций в одном файле с их декларациями. И эти определения должны быть включены (include) в каждый .cc файл, использующий их, иначе могут быть ошибки линковки на некоторых конфигурациях сборки. Если же декларации и определения находятся в разных файлах, включение одного должно подключать другой. Не выделяйте определения в отдельные заголовочные файлы (-inl.h). Раньше такая практика была очень популярна, сейчас это нежелательно.

Как исключение, если из шаблона создаются все доступные варианты шаблонных аргументов или если шаблон реализует функционал, используемый только одним классом — тогда допустимо определять шаблон в одном (и только одном) .cc файле, в котором этот шаблон и используется.

Возможны редкие ситуации, когда заголовочный файл не самодостаточный. Это может происходить, когда файл подключается в нестандартном месте, например в середине другого файла. В этом случае может отсутствовать блокировка от повторного включения, и дополнительные заголовочные файлы также могут не подключаться. Именуйте такие файлы расширением .inc. Используйте их парой и старайтесь чтобы они максимально соответствовали общим требованиям.

Блокировка от повторного включения

Все заголовочные файлы должны быть с защитой от повторного включения посредством #define. Формат макроопределения должен быть:<PROJECT>_<PATH>_<FILE>_H_.

Для гарантии уникальности, используйте компоненты полного пути к файлу в дереве проекта. Например, файл foo/src/bar/baz.h в проекте foo может иметь следующую блокировку:

Предварительное объявление

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

Определение
«Предварительное объявление» — декларация класса, функции, шаблона без соответствующего определения.

  • Предварительной объявление может уменьшить время компиляции. Использование #include требует от компилятора сразу открывать (и обрабатывать) больше файлов.
  • Предварительное объявление позволит избежать ненужной перекомпиляции. Применение #include может привести к частой перекомпиляции из-за различных изменений в заголовочных файлах.
  • Старайтесь избегать предварительного объявления сущностей, объявленных в другом проекте.
  • Когда используйте функцию, объявленную в заголовочном файле, всегда #include этот файл.
  • Когда используйте шаблон класса, предпочтительно #include его заголовочный файл.

Встраиваемые (inline) функции

Определяйте функции как встраиваемые только когда они маленькие, например не более 10 строк.

Определение

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

Использование встраиваемых функций может генерировать более эффективный код, особенно когда функции маленькие. Используйте эту возможность для get/set функций, других коротких и критичных для производительности функций.

Чрезмерное использование встраиваемых функций может сделать программу медленнее. Также встраиваемые функции, в зависимости от размера её, могут как увеличить, так и уменьшить размер кода. Если это маленькие функции, то код может быть уменьшен. Если же функция большая, то размер кода может очень сильно вырасти. Учтите, что на современных процессорах более компактный код выполняется быстрее благодаря лучшему использованию кэша инструкций.

Хорошим правилом будет не делать функции встраиваемыми, если они превышают 10 строк кода. Избегайте делать встраиваемыми деструкторы, т.к. они неявно могут содержать много дополнительного кода: вызовы деструкторов переменных и базовых классов!

Ещё одно хорошее правило: обычно нет смысла делать встраиваемыми функции, в которых есть циклы или операции switch (кроме вырожденных случаев, когда цикл или другие операторы никогда не выполняются).

Важно понимать, что встраиваемая функция не обязательно будет скомпилирована в код именно так. Например, обычно виртуальные и рекурсивные функции компилируются со стандартным вызовом. Вообще, рекурсивные функции не должны объявляться встраиваемыми. Основная же причина делать встраиваемые виртуальные функции — разместить определение (код) в самом определении класса (для документирования поведения или удобства чтения) — часто используется для get/set функций.

Имена и Порядок включения (include)

Вставляйте заголовочные файлы в следующем порядке: парный файл (например, foo.h — foo.cc), системные файлы C, стандартная библиотека C++, другие библиотеки, файлы вашего проекта.

Все заголовочные файлы проекта должны указываться относительно директории исходных файлов проекта без использования таких UNIX псевдонимов как . (текущая директория) или .. (родительская директория). Например, google-awesome-project/src/base/logging.h должен включаться так:

Другой пример: если основная функция файлов dir/foo.cc иdir/foo_test.cc это реализация и тестирование кода, объявленного в dir2/foo2.h, то записывайте заголовочные файлы в следующем порядке:

  1. dir2/foo2.h.
  2. — Пустая строка
  3. Системные заголовочные файлы C (точнее: файлы с включением угловыми скобками с расширением .h), например <unistd.h>, <stdlib.h>.
  4. — Пустая строка
  5. Заголовочные файлы стандартной библиотеки C++ (без расширения в файлах), например <algorithm>, <cstddef>.
  6. — Пустая строка
  7. Заголовочные .h файлы других библиотек.
  8. Файлы .h вашего проекта.

Такой порядок файлов позволяет выявить ошибки, когда в парном заголовочном файле (dir2/foo2.h) пропущены необходимые заголовочные файлы (системные и др.) и сборка соответствующих файлов dir/foo.cc или dir/foo_test.cc завершится ошибкой. Как результат, ошибка сразу же появится у разработчика, работающего с этими файлами (а не у другой команды, которая только использует внешнюю библиотеку).

Обычно парные файлы dir/foo.cc и dir2/foo2.h находятся в одной директории (например, base/basictypes_test.cc и base/basictypes.h), хотя это не обязательно.

Учтите, что заголовочные файлы C, такие как stddef.h обычно взаимозаменяемы соответствующими файлами C++ (cstddef). Можно использовать любой вариант, но лучше следовать стилю существующего кода.

Внутри каждой секции заголовочные файлы лучше всего перечислять в алфавитном порядке. Учтите, что ранее написанный код может не следовать этому правилу. По возможности (например, при исправлениях в файле), исправляйте порядок файлов на правильный.

Следует включать все заголовочные файлы, которые объявляют требуемые вам типы, за исключением случаев предварительного объявления. Если ваш код использует типы из bar.h, не полагайтесь на то, что другой файл foo.h включает bar.h и вы можете ограничиться включением только foo.h: включайте явно bar.h (кроме случаев, когда явно указано (возможно, в документации), что foo.h также выдаст вам типы из bar.h).

Например, список заголовочных файлов в google-awesome-project/src/foo/internal/fooserver.cc может выглядеть так:

Бывают случаи, когда требуется включение заголовочных файлов в зависимости от условий препроцессора (например, в зависимости от используемой ОС). Такое включение старайтесь делать как можно короче (локализованно) и размещать после других заголовочных файлов. Например:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *