Часть 1. Вступление
Часть 2. Заголовочные файлы
…
Все мы при написании кода пользуемся правилами оформления кода. Иногда изобретаются свои правила, в других случаях используются готовые стайлгайды. Хотя все 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).
Определяйте функции как встраиваемые только когда они маленькие, например не более 10 строк.
Определение
Вы можете объявлять функции встраиваемыми и указать компилятору на возможность включать её напрямую в вызывающий код, помимо стандартного способа с вызовом функции.
За
Использование встраиваемых функций может генерировать более эффективный код, особенно когда функции маленькие. Используйте эту возможность для get/set функций, других коротких и критичных для производительности функций.
Против
Чрезмерное использование встраиваемых функций может сделать программу медленнее. Также встраиваемые функции, в зависимости от размера её, могут как увеличить, так и уменьшить размер кода. Если это маленькие функции, то код может быть уменьшен. Если же функция большая, то размер кода может очень сильно вырасти. Учтите, что на современных процессорах более компактный код выполняется быстрее благодаря лучшему использованию кэша инструкций.
Вердикт
Хорошим правилом будет не делать функции встраиваемыми, если они превышают 10 строк кода. Избегайте делать встраиваемыми деструкторы, т.к. они неявно могут содержать много дополнительного кода: вызовы деструкторов переменных и базовых классов!
Ещё одно хорошее правило: обычно нет смысла делать встраиваемыми функции, в которых есть циклы или операции switch (кроме вырожденных случаев, когда цикл или другие операторы никогда не выполняются).
Важно понимать, что встраиваемая функция не обязательно будет скомпилирована в код именно так. Например, обычно виртуальные и рекурсивные функции компилируются со стандартным вызовом. Вообще, рекурсивные функции не должны объявляться встраиваемыми. Основная же причина делать встраиваемые виртуальные функции — разместить определение (код) в самом определении класса (для документирования поведения или удобства чтения) — часто используется для get/set функций.
Вставляйте заголовочные файлы в следующем порядке: парный файл (например, foo.h — foo.cc), системные файлы C, стандартная библиотека C++, другие библиотеки, файлы вашего проекта.
Все заголовочные файлы проекта должны указываться относительно директории исходных файлов проекта без использования таких UNIX псевдонимов как . (текущая директория) или .. (родительская директория). Например, google-awesome-project/src/base/logging.h должен включаться так:
Другой пример: если основная функция файлов dir/foo.cc иdir/foo_test.cc это реализация и тестирование кода, объявленного в dir2/foo2.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 может выглядеть так:
Исключения
Бывают случаи, когда требуется включение заголовочных файлов в зависимости от условий препроцессора (например, в зависимости от используемой ОС). Такое включение старайтесь делать как можно короче (локализованно) и размещать после других заголовочных файлов. Например:
Примечания:
Изображение взято из открытого источника.
Часть 2. Заголовочные файлы
…
Все мы при написании кода пользуемся правилами оформления кода. Иногда изобретаются свои правила, в других случаях используются готовые стайлгайды. Хотя все 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 может иметь следующую блокировку:
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
...
#endif // FOO_BAR_BAZ_H_
Предварительное объявление
По возможности, не используйте предварительное объявление. Вместо этого делайте #include необходимых заголовочных файлов.
Определение
«Предварительное объявление» — декларация класса, функции, шаблона без соответствующего определения.
За
- Предварительной объявление может уменьшить время компиляции. Использование #include требует от компилятора сразу открывать (и обрабатывать) больше файлов.
- Предварительное объявление позволит избежать ненужной перекомпиляции. Применение #include может привести к частой перекомпиляции из-за различных изменений в заголовочных файлах.
Против
- Предварительное объявление может скрывать от перекомпиляции зависимости, которые изменились.
- При изменении API, предварительное объявление может стать некорректным. Как результат, предварительное объявление функция или шаблонов может блокировать изменение API: замена типов параметров на похожий, добавление параметров по умолчанию в шаблон, перенос в новое пространство имён.
- Предварительное объявление символов из std:: может вызвать неопределённое поведение.
- Иногда тяжело понять, что лучше подходит: предварительное объявление или обычный #include. Однако, замена #include на предварительное объявление может (без предупреждений) поменять смысл кода:
// b.h: struct B {}; struct D : B {}; // good_user.cc: #include "b.h" void f(B*); void f(void*); void test(D* x) { f(x); } // calls f(B*)
Если в коде заменить #include на предварительное объявление для структур B и D, то test() будет вызывать f(void*). - Предварительное объявление множества сущностей может быть чересчур объёмным, и может быть проще подключить заголовочный файл.
- Структура кода, допускающая предварительное объявление (и, далее, использование указателей в качестве членов класса) может сделать код запутанным и медленным.
Вердикт
- Старайтесь избегать предварительного объявления сущностей, объявленных в другом проекте.
- Когда используйте функцию, объявленную в заголовочном файле, всегда #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 должен включаться так:
#include "base/logging.h"
Другой пример: если основная функция файлов dir/foo.cc иdir/foo_test.cc это реализация и тестирование кода, объявленного в dir2/foo2.h, то записывайте заголовочные файлы в следующем порядке:
- dir2/foo2.h.
- — Пустая строка
- Системные заголовочные файлы C (точнее: файлы с включением угловыми скобками с расширением .h), например <unistd.h>, <stdlib.h>.
- — Пустая строка
- Заголовочные файлы стандартной библиотеки C++ (без расширения в файлах), например <algorithm>, <cstddef>.
- — Пустая строка
- Заголовочные .h файлы других библиотек.
- Файлы .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 может выглядеть так:
#include "foo/server/fooserver.h"
#include <sys/types.h>
#include <unistd.h>
#include <string>
#include <vector>
#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/server/bar.h"
Исключения
Бывают случаи, когда требуется включение заголовочных файлов в зависимости от условий препроцессора (например, в зависимости от используемой ОС). Такое включение старайтесь делать как можно короче (локализованно) и размещать после других заголовочных файлов. Например:
#include "foo/public/fooserver.h"
#include "base/port.h" // For LANG_CXX11.
#ifdef LANG_CXX11
#include <initializer_list>
#endif // LANG_CXX11
Примечания:
Изображение взято из открытого источника.
IntActment
Хм, из вышеперечисленных правил я не согласен со следующими:
Надеяться, что компилятор узнает о наследуемости без include и использование void* в таком месте — как мне кажется, ССЗБ. Я понимаю, когда void* используют исключительно с POD-типами — я обычно такое заворачиваю в шаблонный метод с std::enable_if_t<std::is_trivially_copyable_v<T>> или добавляю static_assert (предпочитаю этот вариант, ибо можно выкинуть человекопонятное сообщение об ошибке компиляции). Ведь так можно пойти дальше и случайно вызвать delete на таком void* и совершить прочие Undefined Behavior непотрёбства. Вообще, использование void* в плюсах — это как носить заряженный пистолет в кармане. Если уж носите, то не кладите в этот карман ничего другого (перегрузки функций) или купите ему кобуру (обложите static_assert-ами и прочее).
Как правило, решение использовать указатель/ссылку/непостредственно тип при объявлении поля в первую очередь диктуется архитектурными соображениями, аргумент не засчитан
Опять же, основная причина использования forward declaration — избежание перекрестных зависимостей. В случае со стандартной библиотекой этого можно не бояться и смело писать #include<string>.
Насчет inline-функций, как я понял, вообще лучше не использовать без необходимости (необходимостью считаю случаи, когда необходимо написать тело в пределах того же заголовочного файла, но по причине того, что взаимноконвертируемые типы ссылаются друг на друга, тело методов нужно вынести за пределы определение класса ниже объявления зависимого). Вроде как при включенной оптимизации и параметра генерации кода на этапе линковки линковщик сам разберется и заинлайнит все что надо — так я понял из ответов на SO, но все же некоторая неуверенность присутствует… Лично мне не нравится, когда внутренности геттеров торчат в заголовочном файле (в своем пет-проекте я во все функции добавляю макрос с ассертом для проверки текущего потока по маске, геттеры — не исключение). Помимо этого правила, ко всем вышеперечисленным я пришел опытным путем множественного наступания на грабли. Хотелось бы услышать стороннее мнение на этот счет.
virrus
Именно windows.h приходится включать первым, ещё до парного заголовочного файла. Проблема в том, что, например, вызов GetCurrentTime после windows.h превращается в GetCurrentTimeW. Обычно windows.h уходит в precompiled header, так что особых проблем это не вызывает. Интересно, использует (разрешает) ли Гугл precompiled header'ы в своих проектах.
IntActment
Вы хотите сказать, что нижестоящие хедеры ожидают того, что перед ними будет стоять windows.h?
Просто я в своих проектах принципиально не использую precompiled headers, какие у них преимущества?
TargetSan
В зависимости от ситуации могут ускорить компиляцию в несколько раз. Как пример, в одном из моих предыдущих проектов перенос всех заголовочных файлов Qt в PCH привёл к снижению времени компиляции с более получаса до менее 10 минут.
virrus
Нет, не ожидают, но:
1) define-преобразования будут хотя бы одинаковы для всех единиц трансляции
2) Сам windows.h написан так, чтобы быть всегда первым, MS его в шаблонах проектов в stdafx.h кладёт
Apoheliy Автор
Насколько понял позицию Гугла: если вам так нужно windows.h включить до парного заголовочника, то почему бы его не включить в сам парный заголовочник. В нём (парном заголовочнике) используются типы из windows.h или GetCurrentTimeW? — тогда там ему и место!
IntActment
Я даже не знаю, что хуже — включить Windows.h в хедер или написать в хедере «using namespace std»…
Apoheliy Автор
Даже не знаю, что вам предложить: либо не упоминать типы windows в хедере (и тогда его (хедер) можно вставлять первым) или … не упоминать windows в хедере. (Или забить на гугловый кодстайл).
С другой стороны: смотрю на реальный код (Windows, MFC, сгенерённое студией) и в хидерах кастомных компонентов есть инклюды afx…h (в том смысле, что не далеко ушло от windows.h). Как бы и в чём проблема?
Или Вы правы, а я чересчур хочу понять Гугл.
С третьей стороны: они (Гугл) не совсем корпорация …! В исходном документе (ссылка в статье приведена) есть раздел Windows Code. Может имеет смысл глянуть?
IntActment
Я следую такому правилу: если мне дают задачу на проекте, где уже Windows.h включен, то выход один — смириться. Но в таких проектах также и порядок включений может отличаться от предпочитаемого мной — тут, само собой, я буду писать код в соответствии с их кодстайлом. В своих проектах, где я потенциально расчитываю в будущем портировать это на другие платформы, я соблюдаю гигиену — я могу сдублировать typedef'ы из Windows.h, а порой даже (если нужна именно структура оттуда) — в своем заголовочном файле вместо него ввести сырой массив такого же размера, приправленного static_assert(sizeof() == sizeof()) внутри cpp-файла.
dipsy
virrus
Этот GetCurrentTime вообще моя личная функция, заведённая в личном классе. Но define'у на это всё равно. Нельзя быть уверенным, что ни один из тысяч define'ов не совпадет ни с кем из сотен имён в том же stl. Проще подключить всегда и первым, чтобы define-преобразования были идентичными везде.
dipsy
Можно undef сделать, если такие проблемы возникают. В одной замечательной ОС, с которой пришлось работать, были задефайнены Send и Receive, приходилось их раздефайнить и использовать «настоящие» имена функций, __send и __receive. Ну и более лучше задуматься об абстрагировании от конкретной ОС, тогда подобные проблемы возникнут не более 1 раза в модуле, реализующем эту абстракцию.
virrus
Да в общем-то нет проблем. Заменил препроцессор часть токенов — ну флаг ему в руки, гоняться за ним желания нет. windows.h достаточно большой файл.
domix32
так #include включает содержимое файлов в текущий файл, а не наоборот. И проблема возникнет только когда в реализации не найдется системный инклуд в другом файле. Добавить недостающее на порядок тривиальнее на мой взгляд.
gotoxy
По какой причине pragma once в стандарт не принимают?
Oplkill
тут отлично объясняют почему
Ayahuaska
Мои глаза. Это ж машинный перевод, какого-то другого ресурса.
AntonioMichaelAlvarez
Вот оригинал.
Oplkill
почему стековерфлов не подаст в суд на все клоны? Они даже в поисковой выдаче отображаются первее чем оригинал. Дела ведь будут выигрышными
domix32
А смысл? Эффект барабары стрейзанд не остановить. Да и правовые поля неясные — придется помимо технологий еще и кучу юристов обеспечивать.
Playa
Всё, что там написанно про forward declaration — это такой бред, что сложно даже поверить, что это писали в Google.