Использование двойных включений охранников в С++

Итак, у меня недавно было обсуждение, где я работаю, в котором я задавал вопрос об использовании double include guard для одного охранника. Я имею в виду двойную защиту:

Заголовочный файл, "header_a.hpp":

#ifndef __HEADER_A_HPP__
#define __HEADER_A_HPP__
...
...
#endif

При включении файла заголовка в любом месте, в заголовке или исходном файле:

#ifndef __HEADER_A_HPP__
#include "header_a.hpp"
#endif

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

Проблема, которую я не понимаю, использует #ifndef __HEADER_A_HPP__ и #endif вокруг #include "header_a.hpp". Мне сказал коллега, что это добавляет второй уровень защиты к включениям, но я не вижу, как этот второй уровень даже полезен, если первый уровень полностью выполняет работу (или делает это?).

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

Ответ 1

Я уверен, что это плохая практика, чтобы добавить еще один защитник, например:

#ifndef __HEADER_A_HPP__
#include "header_a.hpp"
#endif

Вот несколько причин, по которым:

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

  • Он добавляет ненужные зависимости. Если вы измените включить охрану внутри файла заголовка, вы должны изменить его в всех местах, где заголовок включен.

  • Это, безусловно, не самая дорогая операция, сравнивающая весь процесс компиляции/связи, поэтому он вряд ли может уменьшить общее время сборки.

  • Любой компилятор стоит что-то уже оптимизирует файловую систему include-guard.

Ответ 2

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

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

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

Ответ 3

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

Линкером никак не повлияет.

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

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

P.S. __HEADER_A_HPP__ - это символ, зарезервированный для реализации, поэтому это не то, что вы можете определить. Используйте другое имя для охраны.

Ответ 4

Старые компиляторы на более традиционных (мэйнфреймовых) платформах (мы говорим в середине 2000-х годов здесь) не использовали оптимизацию, описанную в других ответах, и поэтому она действительно использовалась для значительного замедления времени предварительной обработки, прочитайте файлы заголовков, которые уже были включены (имея в виду большой, монолитный, корпоративный проект, в который вы собираетесь включить много файлов заголовков). В качестве примера я видел данные, которые указывают на ускорение в 26 раз для файла с 256 заголовочными файлами, каждый из которых содержит те же 256 файлов заголовков на VisualAge С++ 6 для компилятора AIX (который датируется серединой 2000-х годов). Это довольно экстремальный пример, но этот вид ускорения складывается.

Однако все современные компиляторы, даже на платформах мейнфреймов, таких как AIX и Solaris, выполняют достаточно оптимизацию для включения заголовка, что разница в эти дни действительно незначительна. Поэтому нет веских оснований для этого.

Это, однако, объясняет, почему некоторые компании продолжают придерживаться этой практики, потому что относительно недавно (по крайней мере, в терминах кодовой базы C/С++) все еще было полезно для очень больших монолитных проектов.

Ответ 5

Несмотря на то, что есть люди, которые возражают против этого, на практике "#pragma once" отлично работает, и основные компиляторы (gcc/g++, vС++) поддерживают его.

Итак, какие бы пуристические аргументы люди не распространяли, это работает намного лучше:

  • Fast
  • Без обслуживания, никаких проблем с таинственным не включением, потому что вы скопировали старый флаг
  • Одиночная строка с очевидным значением по сравнению с криптографическими линиями, распространенными в файле

Так просто поставьте:

#pragma once

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