Руководство Google по стилю в C++ (10.2019)

C++ один из основных языков программирования, используемый в open-source проектах Google. Известно, что C++ очень мощный язык. Вместе с тем это сложный язык и, при неправильном использовании, может быть рассадником багов, затруднить чтение и поддержку кода.

Цель руководства – управлять сложностью кода, описывая в деталях как стоит (или не стоит) писать код на C++. Правила этого руководства упростят управление кодом и увеличат продуктивность кодеров.

Style / Стиль – соглашения, которым следует C++ код. Стиль – это больше, чем форматирование файла с кодом.

Большинство open-source проектов, разрабатываемых Google, соответствуют этому руководству.

Примечание: это руководство не является учебником по C++: предполагается, что вы знакомы с языком.

Цели Руководства по стилю

Зачем нужен этот документ?

Есть несколько основных целей этого документа, внутренних Зачем, лежащих в основе отдельных правил. Используя эти цели можно избежать длинных дискуссий: почему правила такие и зачем им следовать. Если вы понимаете цели каждого правила, то вам легче с ними согласиться или отвергнуть, оценить альтернативы при изменении правил под себя.

Цели руководства следующие::

Правила должны стоить изменений
Преимущества от использования единого стиля должны перевешивать недовольство инженеров по запоминанию и использованию правил. Преимущество оценивается по сравнению с кодовой базой без применения правил, поэтому если ваши люди всё равно не будут применять правила, то выгода будет очень небольшой. Этот принцип объясняет почему некоторые правила отсутствуют: например, goto нарушает многие принципы, однако он практически не используется, поэтому Руководство это не описывает.
Оптимизировано для чтения, не для написания
Наша кодовая база (и большинство отдельных компонентов из неё) будет использоваться продолжительное время. Поэтому, на чтение этого кода будет тратиться существенно больше времени, чем на написание. Мы явно заботимся чтобы нашим инженерам было лего читать, поддерживать, отлаживать код. “Оставляй отладочный/логирующий код” – одно из следствий: когда кусок кода работает “странно” (например, при передаче владения указателем), наличие текстовых подсказок может быть очень полезным (std::unique_ptr явно показывает передачу владения).
Пиши код, похожий на существующий
Использование единого стиля на кодовой базе позволяет переключиться на другие, более важные, вопросы. Также, единый стиль способствует автоматизации. И, конечно, автоформат кода (или выравнивание #include-ов) работает правильно, если он соответствует требованиям утилиты. В остальных случаях из набора правил применяется только одно (наиболее подходящее), а некоторая гибкость в использовании правил позволяет людям меньше спорить.
Пиши код, похожий на используемый в C++ сообщества (по возможности)
Согласованность нашего кода с C++ кодом других организаций и сообществ весьма полезна. Если возможности стандартного C++ или принятые идиомы языка облегчают написание программ, это повод использовать их. Однако, иногда стандарт и идиомы плохо подходят для задачи. В этих случаях (как описано ниже) имеет смысл ограничить или запретить использование некоторых стандартных возможностей. В некоторых случаях создаётся свой решение, но иногда используются внешние библиотеки (вместо стандартной библиотеки C++) и переписывание её под свой стандарт слишком затратно.
Избегайте неожиданных или опасных конструкций
В языке C++ есть неочевидные и даже опасные подходы. Некоторые стили кодирования ограничивают их использование, т.к. их использование несёт большие риски для правильности кода.
Избегайте конструкций, которые средний C++ программист считает заумными и сложно поддерживаемыми
В C++ есть возможности, которые в целом не приветствуются по причине усложнения кода. Однако, в часто используемом коде применение хитрых конструкций более оправданно благодаря многократному использованию, также новые порции кода станут более понятны. В случае сомнений – проконсультируйтесь с лидером проекта. Это очень важно для нашей кодовой базы, т.к. владельцы кода и команда поддержки меняются со временем: даже если сейчас все понимают код, через несколько лет всё может исзмениться.
Учитывайте масштаб кода
С кодовой базой более 100 миллионов строк и тысячами инженеров, ошибки и упрощения могут дорого обойтись. Например, важно избегать замусоривания глобального пространства имён: коллизии имён очень сложно избежать в большой базе кода если всё объявляется в глобальном пространстве имён.
Оптимизируйте по необходимости
Оптимизация производительности иногда важнее, чем следование правилам в кодировании.

Намерение этого документа – обеспечить максимально понятное руководство при разумных ограничениях. Как всегда, здравый смысл никто не отменял. Этой спецификацией мы хотим установить соглашения для всего сообщества Google в C++, не только для отдельных команд или людей. Относитесь со скепсисом к хитрым или необычным конструкциям: отсутствие ограничения не всегда есть разрешение. И, если не можешь решить сам, спроси начальника.

Версия C++

Сейчас код должен соответствовать C++17, т.е. возможности C++2x нежелательны. В дальнейшем, руководство будет корректироваться на более новые версии C++.

Не используйте нестандартные расширения.

Учитывайте совместимость с другим окружением, если собираетесь использовать C++14 and C++17 в своём проекте.

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

Желательно, чтобы каждый .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, то записывайте заголовочные файлы в следующем порядке:

  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 может выглядеть так:

#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

Область видимости

Пространство имён

Размещайте свой код в пространстве имён (за некоторыми исключениями). Пространство имён должно иметь уникальное имя, формируемое на основе названия проекта, и, возможно, пути. Не используйте директиву using (например, using namespace foo). Не используйте встроенные (inline) пространства имён. Для безымянных пространств имён смотрите Безымянные пространства имён и статические переменные.

Пространства имён делят глобальную область видимости на отдельные именованные области позволяя избежать совпадения (коллизий) имён.

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

Например, если два разных проекта содержат класс Foo в глобальной области видимости, имена могут конфликтовать. Если каждый проект размещает код в своё пространство имён, то project1::Foo и project2::Foo будут разными именами, конфликтов не будет, в то же время код каждого проекта будет использовать Foo без префикса.

Пространства имён inline автоматически делают видимыми свои имена для включающего пространства имён. Рассмотрим пример кода:

namespace outer {
  inline namespace inner {
    void foo();
  }  // namespace inner
}  // namespace outer

Здесь выражения outer::inner::foo() и outer::foo() взаимозаменяемы. Inline пространства имён в основном используются для ABI-совместимости разных версий.

Пространства имён могут запутать программиста, усложнить понимание, что к чему относится.

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

В ряде случаев требуется использование полных имён и это может сделать код сильно перегруженным.

Используйте пространства имён следующим образом:

  • Следуйте правилам Именования Пространств Имён.
  • В конце объявления пространства имён добавляйте комментарий, аналогично показанным в примерах.
  • Заключайте в пространство имён целиком файл с исходным кодом после #include-ов, объявлений/определений gflag-ов и предварительных объявлений классов из других пространств имён.

    // В .h файле
    namespace mynamespace {
    
    // Все объявления внутри блока пространства имён.
    // Обратите внимание на отсутствие отступа.
    class MyClass {
     public:
      ...
      void Foo();
    };
    
    }  // namespace mynamespace
    
    // В .cc файле
    namespace mynamespace {
    
    // Определение функций внутри блока пространства имён.
    void MyClass::Foo() {
      ...
    }
    
    }  // namespace mynamespace
    

    В .cc файлах могут быть дополнительные объявления, такие как флаги или using-декларации.

    #include "a.h"
    
    ABSL_FLAG(bool, someflag, false, "dummy flag");
    
    namespace mynamespace {
    
    using ::foo::Bar;
    
    ...code for mynamespace...    // Код начинается с самой левой границы.
    
    }  // namespace mynamespace
    
  • Чтобы генерируемый из protobuf-а код был размещён в требуемом пространстве имён, применяйте спецификатор package в .proto файле. Подробнее здесь: Protocol Buffer Packages.
  • Ничего не объявляйте в пространстве std, в том числе и предварительные объявления классов стандартной библиотеки. Объявление в пространстве имён std приведёт к неопределённому поведению (UB) и это будет непереносимый код. Для объявления сущностей используйте соответствующий заголовочный файл.
  • Не используйте using-директиву чтобы сделать доступными все имена из пространства имён.

    // Недопустимо -- Это загрязняет пространство имён.
    using namespace foo;
    
  • Не используйте псевдонимы пространств имён в блоке namespace в заголовочном файле, за исключением явно обозначенных “внутренних” пространств имён. Связано это с тем, что любая декларация в заголовочном файле становится частью публичного API, экспортируемого этим файлом.

    // Укороченная запись для доступа к часто используемым именам в .cc файлах.
    namespace baz = ::foo::bar::baz;
    
    // Укороченная запись для доступа к часто используемым именам (в .h файле).
    namespace librarian {
    namespace impl {  // Внутреннее содержимое, не являющееся частью API.
    namespace sidetable = ::pipeline_diagnostics::sidetable;
    }  // namespace impl
    
    inline void my_inline_function() {
      // Пространство имён, локальное для функции (или метода).
      namespace baz = ::foo::bar::baz;
      ...
    }
    }  // namespace librarian
    
  • Не используйте inline-пространства имён.

Безымянные пространства имён и статические переменные

Когда определения внутри .cc файла не используются в других исходных файлах, размещайте такие определения в безымянном пространстве имён или объявляйте их как static. Не используйте такой тип определения в заголовочных файлах (.h).

Размещённые в безымянном пространстве имён объявления могут быть слинкованы как internal (только для внутреннего использования). Функции и переменные также могут быть с internal линковкой, если они заявлены как static. Такие типы объявления подразумевают, что они будут недоступны из другого файла. Если другой файл объявляет сущность с таким же именем, то оба объявления будут полностью независимы.

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

Формат описания безымянного пространства имён полностью аналогичен именованному варианту. Не забывайте к закрывающей скобке написать комментарий, в котором имя оставьте пустым:

namespace {
...
}  // namespace

Функции: глобальные, статические внутри класса, вне класса

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

Вообще функции (как статические, так и вне класса) довольно полезная штука, Кэп. И размещение функций либо в классе, либо в пространстве имён позволяет всё остальное содержать в чистоте (я про глобальное пространство имён).

Иногда разумнее статические функции класса и функции вне класса сгруппировать в одном месте, в новом классе. Например, когда у них сложные зависимости от всего или им нужен доступ к внешним ресурсам.

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

Если требуется определить функцию: в .cc-файле; вне класса; используемой только в локальном файле – используйте internal линковку для ограничения области видимости.

Локальные переменные

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

Язык C++ позволяет объявлять переменные в любом месте функции. Однако рекомендуется делать это в наиболее узкой (наиболее вложенной) области видимости, и по возможности ближе к первому использованию. Это облегчает поиск объявлений, проще узнать тип переменной и её начальное значение. Также рекомендуется использовать инициализацию, а не объявление с присваиванием. Примеры:

int i;
i = f();      // Плохо -- инициализация отделена от объявления.
int j = g();  // Хорошо -- объявление с инициализацией.
std::vector<int> v;
v.push_back(1);  // Желательно инициализировать с помощью {}.
v.push_back(2);
std::vector<int> v = {1, 2};  // Хорошо -- v сразу инициализирован.

Переменные, необходимые только внутри кода if, while и for лучше объявлять внутри условий, тогда область их видимости будет ограничена только соответствующим блоком кода:

while (const char* p = strchr(str, '/')) str = p + 1;

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

// Неэффективная реализация:
for (int i = 0; i < 1000000; ++i) {
  Foo f;  // Конструктор и деструктор Foo вызовутся по 1000000 раз каждый.
  f.DoSomething(i);
}

Возможно было бы более эффективно такую переменную (которая используется внутри цикла) объявить вне цикла:

Foo f;  // Конструктор и деструктор Foo вызовутся по разу.
for (int i = 0; i < 1000000; ++i) {
  f.DoSomething(i);
}

Переменные: статические и глобальные

Объекты в статической области видимости/действия запрещены, кроме тривиально удаляемых. Фактически это означает, что деструктор должен ничего не делать (включая вложенные или базовые типы). Формально это можно описать, что тип не содержит пользовательского или виртуального деструктора и что все базовые типы и не-статические члены ведут себя аналогично (т.е. являются тривиально удаляемыми). Статические переменные в функциях могут быть динамически инициализированными. Использование же динамической инициализации для статических членов класса или переменных в области пространства имён (namespace) в целом не рекомендуется, однако допустимо в ряде случаев (см. ниже).

Эмпирическое правило: если глобальную переменную (рассматривая её изолированно) можно объявить как constexpr, значить она соответствует вышеуказанным требованиям.

Каждый объект имеет тот или иной тип времени жизни / storage duration, и, очевидно, это влияет на время жизни объекта. Объекты статического типа доступны с момента их инициализации до момента завершения программы. Такие объекты могут быть переменными в пространстве имён (“глобальные переменные”), статическими членами классов, локальными переменными внутри функций со спецификатором static. Статические переменные в функциях инициализируются, когда поток выполнения кода проходит в первый раз через объявление; все остальные объекты статического типа инициализируются в фазе старта (start-up) приложения. Все объекты статического типа удаляются в фазе завершения программы (до обработки незавершённых(unjoined) потоков).

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

Глобальные и статические переменные бывают очень полезными: константные имена, дополнительные структуры данных, флаги командной строки, логирование, регистрирование, инфраструктура и др.

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

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

const int kNum = 10;  // Допустимо

struct X { int n; };
const X kX[] = {{1}, {2}, {3}};  // Допустимо

void foo() {
  static const char* const kMessages[] = {"hello", "world"};  // Допустимо
}

// Допустимо: constexpr всегда имеет тривиальный деструктор
constexpr std::array<int, 3> kArray = {{1, 2, 3}};
// Плохо: нетривиальный деструктор
const std::string kFoo = "foo";

// Плохо по тем же причинам (хотя kBar и является ссылкой, но
// правило применяется и для временных объектов в расширенным временем жизни)
const std::string& kBar = StrCat("a", "b", "c");

void bar() {
  // Плохо: нетривиальный деструктор
  static std::map<int, int> kData = {{1, 0}, {2, 0}, {3, 0}};
}

Отметим, что ссылка не есть сам объект, и, следовательно, к ним не применяются ограничения по разрушению объекта. Хотя ограничения на динамическую инициализацию остаются в силе. В частности, внутри функции допустим следующий код static T& t = *new T;.

Тонкости инициализации

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

int n = 5;    // Отлично
int m = f();  // ? (Зависит от f)
Foo x;        // ? (Зависит от Foo::Foo)
Bar y = g();  // ? (Зависит от g и Bar::Bar)

На выполнение всех выражений, кроме первого, может повлиять порядок инициализации, который может быть разным/неопределённым (или зависимым от …).

Рассмотрим константную инициализацию. Это означает, что инициализационное выражение – константное, и если при создании объекта вызывается конструктор, то он (конструктор) тоже должен быть заявлен как constexpr:

struct Foo { constexpr Foo(int) {} };

int n = 5;  // Отлично, 5 - константное выражение
Foo x(2);   // Отлично, 2 - константное выражение и вызывается constexpr конструктор
Foo a[] = { Foo(1), Foo(2), Foo(3) };  // Отлично

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

Например, следующие инициализации могут привести к проблемам:

// Объявления
time_t time(time_t*);      // не constexpr !
int f();                   // не constexpr !
struct Bar { Bar() {} };

// Проблемные инициализации
time_t m = time(nullptr);  // Инициализационное выражение не константное
Foo y(f());                // Те же проблемы
Bar b;                     // Конструктор Bar::Bar() не является constexpr

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

int p = getpid();  // Допустимо, пока другие статические переменные
                   // не используют p в своей инициализации

Динамическая же инициализация статических переменных в функциях (локальных) допустима и является широко распространённой практикой.

Стандартные практики

  • Глобальные строки: если требуется глобальная или статическая строковая константа, то рекомендуется использовать простой символьный массив или указатель на первый символ строкового литерала. Строковые литералы обычно находятся в статическом размещении (их время жизни) и этого в большинстве случаев достаточно.
  • Динамические контейнеры (map, set и т.д.): если требуется статическая коллекция с фиксированными данными (например, таблицы значений для поиска), то не используйте динамические контейнеры из стандартной библиотеки как тип для статической переменной, т.к. у этих контейнеров нетривиальный деструктор. Вместо этого попробуйте использовать массивы простых (тривиальных) типов, например массив из массивов целых чисел (вместо std::map<int, int>) или, например, массив структур с полями int и const char*. Учтите, что для небольших коллекций линейный поиск обычно вполне приемлем (и может быть очень эффективным благодаря компактному размещению в памяти). Также можете воспользоваться алгоритмами absl/algorithm/container.h для стандартных операций. Также возможно создавать коллекцию данных уже отсортированной и использовать алгоритм бинарного поиска. Если без динамического контейнера не обойтись, то попробуйте использовать статическую переменную-указатель, объявленную в функции (см. ниже).
  • Умные указатели (unique_ptr, shared_ptr): умные указатели освобождают ресурсы в деструкторе и поэтому использовать их нельзя. Попробуйте применить другие практики/способы, описанные в разделе. Например, одно из простых решений это использовать обычный указатель на динамически выделенный объект и далее никогда не удалять его (см. последний вариант списка).
  • Статические переменные пользовательского типа: если требуется статический и константный пользовательский тип, заполненный данными, то можете объявить у этого типа тривиальный деструктор и constexpr конструктор.
  • Если все другие способы не подходят, то можно создать динамический объект и никогда не удалять его. Объект можно создать с использованием статического указателя или ссылки, объявленной в функции, например: static const auto& impl = *new T(args...);.

Потоковые переменные

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

Начиная с C++11 переменные можно объявлять со спецификатором thread_local:

thread_local Foo foo = ...;

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

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

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

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

  • Операции с thread_local переменными могут привести к выполнению кода, который не предполагался и его размер также может быть различным.
  • Переменные thread_local являются по сути глобальными переменными со всеми их недостатками (за исключением потокобезопасности).
  • Выделяемая для thread_local память зависит от количества потоков и её размер может быть значительным.
  • Обыкновенный член класса не может быть thread_local.
  • В ряде случаев thread_local переменные могут проигрывать по эффективности хорошо оптимизированному компилятором коду (например, интринсикам/intrinsics).

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

Foo& MyThreadLocalFoo() {
  thread_local Foo result = ComplicatedInitialization();
  return result;
}

Переменные thread_local в классе или пространстве имён необходимо инициализировать константой времени компиляции (т.е. динамическая инициализация недопустима). Для этого необходимо аннотировать эти thread_local переменные с помощью ABSL_CONST_INIT (или constexpr, но это лучше использовать пореже):

ABSL_CONST_INIT thread_local Foo foo = ...;

Переменные thread_local должны быть предпочтительным способом определения потоковых данных.

Классы

Классы являются основным строительным блоком в C++. И, конечно же, используются они часто. В этой секции описаны основные правила и запреты, которым нужно следовать при использовании классов.

Код в конструкторе

Не вызывайте виртуальные методы в конструкторе. Избегайте инициализации, которая может завершиться ошибкой (а способа сигнализировать об ошибке не предусмотрено. Прим.: учтите, что Гугл не любит исключения).

Вообще в конструкторе можно выполнять любые инициализации (т.е. всю инициализацию сделать в конструкторе).

  • Не нужно беспокоиться об неинициализированном классе.
  • Объекты, которые полностью инициализируются в конструкторе, могут быть константными (const) и также их легче использовать в стандартных контейнерах и алгоритмах.

  • Если в конструкторе вызываются виртуальные функции, то не вызываются реализации из производного класса. Даже если сейчас класс не имеет потомков, в будущем это может обернуться проблемой.
  • Нет простого способа проинформировать об ошибке без крэша программы (что не всегда допустимо) или выбрасывания исключений (которые запрещены).
  • Если возникла ошибка, то у нас есть частично (обычно – неправильно) инициализированный объект. Очевидное действие: добавить механизм проверки состояния bool IsValid(). Однако про эту проверку легко забыть.
  • Вы не можете пользоваться адресом конструктора. Поэтому нет, например, простого способа передать выполнение конструктора в другой поток.

Конструкторы не должны вызывать виртуальные функции. В ряде случаев (если это позволительно) обработка ошибок конструирования возможна через завершение программы. В иных случаях рассмотрите паттерн Фабричный Метод или используйте Init() (подробнее здесь: TotW #42). Используйте Init() только в случае, если у объекта есть флаги состояния, разрешающие вызывать те или иные публичные функции (т.к. сложно полноценно работать с частично сконструированным объектом).

Неявные преобразования

Не объявляейте неявные преобразования. Используйте ключевое слово explicit для операторов преобразования типа и конструкторов с одним аргументом.

Неявные преобразования позволяют объект одного типа (source type) использовать там, где ожидается другой тип (destination type), например передача аргумента типа int в функцию, ожидающую double.

Помимо неявных преобразований, задаваемых языком программирования, можно также определять свои пользовательские, добавляя соответствующие члены в объявление класса (как источника, так и получателя). Неявное преобразование на стороне источника объявляется как оператор + тип получателя (например, operator bool()). Неявное преобразование на стороне получателя реализуется конструктором, принимающим тип источника как единственный аргумент (помимо аргументов со значениями по умолчанию).

Ключевое слово explicit может применяться к конструктору или к оператору преобразования для явного указания, что функция может применяться только при явном соответствии типов (например, после операции приведения). Это применяется не только для неявного преобразования, но и для списков инициализации в C++11:

class Foo {
  explicit Foo(int x, double y);
  ...
};

void Func(Foo f);
Func({42, 3.14});  // Ошибка

Этот пример кода технически не является неявным преобразованием, но язык трактует это как будто подразумевается explicit.

  • Неявные преобразования могут сделать тип более удобным в использовании, не требуя явного указания типа в очевидных случаях.
  • Неявные преобразования могут быть упрощённой альтернативой для перегрузки, например когда одна функция с аргументом типа string_view применяется вместо отдельных версий для std::string и const char*.
  • Применение списка инициализации является компактным и понятным способом инициализации объектов.

  • Неявные преобразования могут скрывать баги с несоответствием типов, когда получаемый тип не соответствует ожиданиям пользователя (если он вообще предополагал приведение типа).
  • Неявные преобразования могут усложнить чтение кода, особенно при наличии перегруженных функций: становится неясно, какой код действительно будет вызван.
  • Конструкторы с одним аргументом могут быть случайно использованы как неявное преобразование, даже если это не предполагалось.
  • Когда конструктор с одним аргументом не объявлен как explicit нельзя с уверенностью сказать: то ли это такое неявное преобразование, то ли автор забыл ключевое слово.
  • Не всегда понятно, какой тип должен обеспечить преобразование. А если – оба, код становится двусмысленным.
  • Использование списка инициализации также может добавить проблем, если целевой тип задан неявно и, особенно, если сам список состоит только из одного элемента.

Операторы преобразования типа и конструкторы с одним аргументом должны объявляться с ключевым словом explicit. Есть и исключение: конструкторы копирования и перемещения могут объявляться без explicit, т.к. они не выполняют преобразование типов. Ещё неявные преобразования могут быть необходимы в случае классов-обёрток для других типов (в этом случае обязательно запросите разрешение у вышестоящего руководства на возможность игнорирования этого важного правила).

Конструкторы, которые нельзя вызвать с одним аргументом, можно объявлять без explicit. Конструкторы, принимающие единственный std::initializer_list также должны объявляться без explicit для поддержки инициализации копированием (например, MyType m = {1, 2};).

Копируемые и перемещаемые типы

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

Перемещаемый тип – тот, что может быть инициализирован или присвоен из временных значений.

Копируемый тип – может быть инициализирован или присвоен из другого объекта того же типа (т.е. также, как и перемещаемый), с условием, что исходный объект остаётся неизменным. Например, std::unique_ptr<int> – это перемещаемый, но не копируемый тип (т.к. значение исходного std::unique_ptr<int> объекта должно измениться при присвоении целевому объекту). int и std::string – примеры перемещаемый типов, которые также можно копировать: для int операции перемещения и копирования одинаковые, для std::string операция перемещения требует меньше ресурсов, чем копирование.

Для пользовательских типов копирование задаётся конструктором копирования и оператором копирования. Перемещение задаётся либо конструктором перемещения с оператором перемещения, либо (если их нет) соответствующими функциями копирования.

Конструкторы копирования и перемещения могут неявно вызываться компилятором, например при передаче объектов по значению.

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

Конструкторы копирования/перемещения и соответствующие операторы присваивания обычно легче определить, чем альтернативы наподобие Clone(), CopyFrom() или Swap(), т.к. компилятор может сгенерировать требуемые функции (неявно или посредством = default). Они (функции) легко объявляются и можно быть уверенным, что все члены класса будут скопированы. Конструкторы (копирования и перемещения) в целом более эффективны, т.к. не требуют выделения памяти, отдельной инициализации, дополнительных присвоений, хорошо оптимизируются (см. copy elision).

Операторы перемещения позволяют эффективно (и неявно) управлять ресурсами rvalue объектов. Иногда это упрощает кодирование.

Некоторым типам не требуется быть копируемыми, и поддержка операций копирования может противоречить логике или привести к некорректной работе. Типы для синглтонов (Registerer), объекты для очистки (например, при выходе за область видимости) (Cleanup) или содержащие уникальные данные (Mutex) по своему смыслу являются некопируемыми. Также, операции копирования для базовых классов, имеющих наследников, могут могут привести к “нарезке объекта” object slicing. Операции копирования по умолчанию (или неаккуратно написаные) могут привести ошибкам, которые тяжело обнаружить.

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

Открытый интерфейс каждого класса должен явно указывать, какие операции копирования и/или перемещения он поддерживает. Обычно это делается в секции public в виде явных деклараций нужных функций или объявлением их как delete.

В частности, копирумый класс должен явно объявлять операции копирования; только перемещаемый класс должен явно объявить операции перемещения; некопируемый/неперемещаемый класс должен явно запретить (= delete) операции копирования. Явная декларация или удаление всех четырёх функций копирования и перемещения также допустима, хотя это и не требуется. Если вы реализуете оператор копирования и/или перемещения, то необходимо также сделать соответствующий конструктор.

class Copyable {
 public:
  Copyable(const Copyable& other) = default;
  Copyable& operator=(const Copyable& other) = default;

  // Неявное определение операций перемещения будет запрещено (т.к. объявлено копирование)
};

class MoveOnly {
 public:
  MoveOnly(MoveOnly&& other);
  MoveOnly& operator=(MoveOnly&& other);

  // Неявно определённые операции копирования удаляются. Но (если хотите) можно это записать явно:
  MoveOnly(const MoveOnly&) = delete;
  MoveOnly& operator=(const MoveOnly&) = delete;
};

class NotCopyableOrMovable {
 public:
  // Такое объявление запрещает и копирование и перемещение
  NotCopyableOrMovable(const NotCopyableOrMovable&) = delete;
  NotCopyableOrMovable& operator=(const NotCopyableOrMovable&)
      = delete;

  // Хотя операции перемещения запрещены (неявно), можно записать это явно:
  NotCopyableOrMovable(NotCopyableOrMovable&&) = delete;
  NotCopyableOrMovable& operator=(NotCopyableOrMovable&&)
      = delete;
};

Описываемые объявления или удаления функций можно опустить в очевидных случаях:

  • Если класс не содержит секции private (например, структура struct или класс-интерфейс), то копируемость и перемещаемость можно заявить через аналогичное свойство любого открытого члена.
  • Если базовый класс явно некопируемый и неперемещаемый, наследные классы будут такими же. Однако, если базовый класс не объявляет это операции, то этого будет недостаточно для прояснения свойств наследуемых классов.
  • Заметим, что если (например) конструктор копирования объявлен/удалён, то нужно и явно объявить/удалить оператор копирования (т.к. его статус неочевиден). Аналогично и для объявления/удаления оператора копирования. Аналогично и для операций перемещения.

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

Из-за риска “слайсинга” предпочтительным будет избегать открытых операторов копирования и перемещения для классов, которые планируется использовать в качестве базовых (и предпочтительно не наследоваться от класса с такими функциями). Если же необходимо сделать базовый класс копируемым, то сделайте открытую виртуальную функцию Clone() и защищённый (protected) конструктор копий с тем, чтобы производный класс мог их использовать для реализации операций копирования.

Структуры vs Классы

Используйте структуры (struct) только для пассивных объектов, хранящих данные. В других случаях используйте классы (class).

Ключевые слова struct и class практически идентичны в C++. Однако, у нас есть собственное понимание для каждого ключевого слова, поэтому используйте то, которое подходит по назначению и смыслу.

Структуры должны использоваться для пассивных объектов, только для переноса данных. Они могут иметь собственные константы, однако не должно быть никакой функциональности (за, возможно, исключением функций get/set). Все поля должны быть открытыми (public), доступны для прямого доступа и это более предпочтительно, чем использование функций get/set. Структуры не должны содержать инварианты (например, вычисленные значения), которые основаны на зависимости между различными полями структуры: возможность напрямую изменять поля может сделать инвариант невалидным. Методы не должны ограничивать использование структуры, но могут присваивать значения полям: т.е. как конструктор, деструктор или функции Initialize(), Reset().

Если требуется дополнительная функциональность в обработке данных или инварианты, то предпочтительно применение классов (class). Также, если сомневаетесь, что выбрать – используйте классы.

В ряде случаев (шаблонные мета-функции, traits, некоторые функторы) для единообразия с STL допускается использование структур вместо классов.

Не забудьте, что переменные в структурах и классах именуются разными стилями.

Структуры vs пары (pair) и кортежи (tuple)

Если отдельные элементы в блоке данных могут осмысленно называться, то желательно использовать структуры вместо пар или кортежей.

Хотя использование пар и кортежей позволяет не изобретать велосипед с собственным типом и сэкономит много времени при написании кода, поля с осмысленными именами (вместо .first, .second или std::get<X>) будут более понятны при чтении кода. И хотя C++14 для кортежей в дополнение к доступу по индексу добавляется доступ по типу (std::get<Type>, а тип должен быть уникальным), имя поля намного более информативно нежели тип.

Пары и кортежи являются подходящими в коде, где нет специального различия между элементами пары или кортежа. Также они требуются для работы с существующим кодом или API.

Наследование

Часто композиция класса более подходяща, чем наследование. Когда используйте наследование, делайте его открытым (public).

Когда дочерний класс наследуется от базового, он включает определения всех данных и операций от базового. “Наследование интерфейса” – это наследование от чистого абстрактного базового класса (в нём не определены состояние или методы). Всё остальное – это “наследование реализации”.

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

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

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

Любое наследование должно быть открытым (public). Если хочется сделать закрытое (private), то лучше добавить новый член с экземпляром базового класса.

Не злоупотребляйте наследованием реализации. Композиция классов часто более предпочтительна. Попытайтесь ограничить использование наследования семантикой “Является”: Bar можно наследовать от Foo, если можно сказать, что Bar “Является” Foo (т.е. там, где используется Foo, можно также использовать и Bar).

Защищёнными (protected) делайте лишь те функции, которые должны быть доступны для дочерних классов. Заметьте, что данные должны быть закрытыми (private).

Явно декларируйте переопределение виртуальных функций/деструктора с помошью спецификаторов: либо override, либо (если требуется) final. Не используйте спецификатор virtual при переопределении функций. Объяснение: функция или деструктор, помеченные override или final, но не являющиеся виртуальными просто не скомпилируются (что помогает обнаружить общие ошибки). Также спецификаторы работают как документация; а если спецификаторов нет, то программист будет вынужден проверить всю иерархию, чтобы уточнить виртуальность функции.

Множественное наследование допустимо, однако множественное наследование реализации не рекомендуется от слова совсем.

Перегрузка операторов

Перегружайте операторы в рамках разумного. Не используйте пользовательские литералы.

C++ позволяет пользовательскому коду переопределять встроенные операторы используя ключевое слово operator и пользовательский типа как один из параметров; также operator позволяет определять новые литералы, используя operator""; также можно создавать функции приведения типов, наподобие operator bool().

Использование перегрузки операторов для пользовательских типов (по аналогии со встроенными типами) может сделать код более сжатым и интуитивным. Перегружаемые операторы соответствуют определённым операциям (например, ==, <, = и <<) и если код следует логике применения этих операций, то пользовательские типы можно сделать понятнее и использовать при работе с внешними библиотеками, которые опираются на эти операции.

Пользовательские литералы – очень эффективный способ для создания пользовательских объектов.

  • Написать комплект операторов для класса (корректных, согласованных и логичных) – это может потребовать известных усилий и, при недоработанном коде, это может обернуться труднопонимаемыми багами.
  • Излишняя перегрузка операторов может усложнить понимание кода, особенно если код не соответствует логике операции.
  • Все недостатки, связанные с перегрузкой функций, присущи и перегрузке операторов.
  • Перегрузка операторов обмануть других программистов, ожидающих простую и быструю встроенную операцию, а получающие нечто ресурсоёмкое.
  • Поиск мест вызова перегруженных операторов может быть нетривиальной задачей, и это явно сложнее обычного текстового поиска.
  • При ошибках в типах аргументов вы можете вместо сообщений об ошибке/предупреждении от компилятора (по которым легко найти проблему и исправить её), получить “корректный” вызов другого оператора. Например, код для foo < bar может сильно отличаться от кода для &foo < &bar; немного напутав в типах получим ошибочный вызов.
  • Перегрузка некоторых операторов является в принципе рискованным занятием. Перегрузка унарного & может привести к тому, что один и тот же код будет трактоваться по разному в зависимости от видимости декларации этой перегрузки. Перегрузка операторов &&, || и , (запятая) может поменять порядок (и правила) вычисления выражений.
  • Часто операторы определяются вне класса, и есть риск использования разных реализаций одного и того же оператора. Если оба определения будут слинкованы в один бинарный файл, можно получить неопределённое поведение и хитрые баги.
  • Пользовательские литералы (UDL) позволяют создавать новые синтаксические формы, незнакомые даже продвинутым C++ программистам. Например: "Hello World"sv как сокращение для std::string_view("Hello World"). Исходная нотация может быть более понятной, хотя и не такой компактной.
  • Т.к. для UDL не указывается пространство имён, потребуется либо использовать using-директиву (которая запрещена) или using-декларацию (которая также запрещена (в заголовочных файлах), кроме случая когда импортируемые имена являются частью интерфейса, показываемого в заголовочном файле). Для таких заголовочных файлов лучше бы избегать суффикусов UDL, и желательно избегать зависимости между литералами, которые различны в заголовочном и исходном файле.

Определяйте перегруженные операторы только если их смысл очевиден, понятен, и соответствует общей логике. Например, используйте | в смысле операции ИЛИ; реализовывать же вместо этого логику канала (pipe) – не очень хорошая идея.

Определяйте операторы только для ваших собственных типов, делайте это в том же самом заголовочном и исходном файле, и в том же пространстве имён. В результате операторы будут доступны там же, где и сами типы, а риск множественного определения минимален. По возможности избегайте определения операторов как шаблонов, т.к. придётся соответствовать любому набору шаблонных аргументов. Если вы определяете оператор, также определяйте “родственные” к нему. И позаботьтесь о согласованности выдаваемых ими результатов. Например, если определяется оператор <, то определяйте все операторы сравнения и проследите, чтобы операторы < и > никогда не возвращали true для один и тех же аргументов.

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

Не нужно пытаться обойти переопределение операторов. Если требуется сравнение (или присваивание и функция вывода), то лучше определить == (или = и <<) вместо своих функций Equals(), CopyFrom() и PrintTo(). И наоборот: не нужно переопределять оператор только потому, что внешние библиотеки ожидают этого. Например, если тип данных нельзя упорядочить и хочется хранить его в std::set, то лучше сделайте пользовательскую функцию сравнения и не пользуйтесь оператором <.

Не переопределяйте &&, ||, , (запятая) или унарный &. Не переопределяйте operator"", т.е. не стоит вводить собственные литералы. Не используйте ранее определённые литералы (включая стандартную библиотеку и не только).

Дополнительная информация:
Преобразование типов описано в секции неявные преобразования. Оператор = расписан в конструкторе копий. Тема перегрузки << для работы со стримами освещена в потоках/streams. Также можно ознакомиться с правилами из раздела перегрузка функций, которые также подходят и для операторов.

Доступ к членам класса

Данные класса делайте всегда закрытыми private, кроме констант. Это упрощает использование инвариантов путём добавления простейших (часто – константных) функций доступа.

Допустимо объявлять данные класса как protected для использования в тестовых классах (например, при использовании Google Test) или других подобных случаях.

Порядок объявления

Располагайте похожие объявления в одном месте, выносите общие части наверх.

Определение класса обычно начинается с секции public:, далее идёт protected: и затем private:. Пустые секции не указывайте.

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

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

Функции

Выходные параметры

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

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

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

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

Порядок объявления параметров не является жёстким правилом. И расположение входных-и-выходных параметров также может меняться. В общем, правило есть, но иногда нужно проявить гибкость.

Пишите короткие функции

Желательно писать маленькие и сфокусированные функции.

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

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

Когда вы работаете с чьим-то кодом, возможно встретятся длинные и запутанные функции. Не нужно боятся модифицировать существующий код: если работа с такой функцией становится запутанной, её сложно отлаживать или необходимо использовать куски этой функции в другом контексте, то попробуйте её разбить на несколько более маленьких кусочков, с которыми проще работать.

Параметры-Ссылки

Все параметры, передаваемые по ссылке (lvalue reference) должны быть помечены как const.

В языке C если функции требуется модифицировать внешнюю переменную, то её необходимо передавать как параметр-указатель, например int foo(int *pval). В C++ для этих целей также можно использовать параметры-ссылки: int foo(int &val).

Определение параметра как ссылку уменьшает количество “уродливого” кода, типа (*pval)++. Параметры-ссылки необходимы для ряда применений, например конструкторов копий. Кроме того, такие параметры явно указываются на недопустимость использования указателей на NULL.

Использование ссылок может запутывать при разборе кода, т.к. они имеют синтаксис значения, но семантику указателя.

В параметрах функции все ссылки должны быть const:

void Foo(const std::string &in, std::string *out);

На самом деле это принципиальное требование в коде Google, что входные аргументы передаются как значения или константные ссылки, а выходные – как указатели. Технически, входные аргументы тоже могут быть константными указателями, но не-константные ссылки не допускаются (за исключением случаев, когда это явно необходимо, например swap()).

Также, в ряде случаев использование const T* более предпочтительно, чем const T& для входных параметров. Например:

  • Требуется передать нулевой указатель.
  • Функция сохраняет указатель или ссылку на входной параметр.

Помните, что в большинстве случаев входные параметры будут передаваться как const T&. Использование const T* будет указанием, что этот параметр немного особенный. Поэтому, если вы выбираете const T* вместо const T&, то делайте это намеренно, с определённой целью. Иначе можно ввести в заблуждение других программистов и они будут искать причину, которой нет.

Перегрузка функций

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

Можно написать функцию, которая принимает аргумент const std::string&. И, например, перегрузить её другой функцией, принимающей const char* (хотя в этом случае лучше использовать std::string_view).

class MyClass {
 public:
  void Analyze(const std::string &text);
  void Analyze(const char *text, size_t textlen);
};

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

Перегрузка с использованием различных вариаций константных и ссылочных аргументов может сделать код более удобным и эффективным (См. TotW 148).

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

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

Аргументы по-умолчанию

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

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

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

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

Параметры по-умолчанию вычисляются для каждой точки вызова заново. И это может увеличить объём генерируемого кода. Однако, обычно ожидается, что значение по-умолчанию всегда одинаковое (т.е. не меняется в от вызова к вызову).

Указатели на функции с аргументами по-умолчанию также могут сбивать с толку, т.к. сигнатура функции часто не соответствует форме вызова. И перегрузка функции позволяет решить эти проблемы.

Аргументы по-умолчанию под запретом для виртуальных функций (где они могут работать некорректно) и для случаев, когда значение для аргумента может измениться. Например, не пишите такой код: void f(int n = counter++);.

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

Синтаксис указания возвращаемого типа в конце

Указывайте возвращаемый тип в конце, только если обычный синтаксис (возвращаемый тип в начале) неудобен или нечитабелен.

В C++ есть две формы декларации функций. В обычной (более старой) форме возвращаемый тип указывается перед именем функции:

int foo(int x);

В C++11 появилась новая форма, использующая auto перед именем функции и завершающаяся возвращаемым типом, указываемым после списка аргументов. Например, предыдущую декларацию можно записать как:

auto foo(int x) -> int;

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

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

Иногда намного проще и удобнее указать возвращаемый тип в конце, после списка параметров. Особенно в случаях, когда возвращаемый тип зависит от параметров шаблона. Например:

    template <typename T, typename U>
    auto add(T t, U u) -> decltype(t + u);
  

понятнее, чем:

    template <typename T, typename U>
    decltype(declval<T&>() + declval<U&>()) add(T t, U u);
  

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

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

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

Специфика Google

Есть различные трюки и средства, которые используются чтобы сделать код на C++ более надёжным. И да, они могут отличаться от того, что используют в других компаниях.

Владение и умные указатели

Предпочтительно, чтобы динамически созданный объект имел одного (выделенного) владельца. Передачу такого “владения” желательно проводить через умные указатели.

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

“Умные” указатели это классы, которые функционируют как обычные указатели; например, в них перегружены операторы * и ->. Некоторые типы умных указателей можно использовать для автоматического управления “владением”: учёт владельцев, удаление объектов. std::unique_ptr это умный указатель, добавленный в C++11, который реализует эксклюзивное владение (только один владелец); объект удаляется в случае выхода из области видимости экземпляра std::unique_ptr. std::unique_ptr не может быть скопирован, однако его можно передать (move) другому std::unique_ptr, что фактически есть передача владения. std::shared_ptr это умный указатель, реализующий распределённое владение. std::shared_ptr можно копировать, при этом владение распределяется между всеми копиями. Управляемый объект удаляется, когда разрушается последняя копия std::shared_ptr.

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

  • Владение должно представляться и передаваться через указатели (любо умные, либо обычные). Семантика указателей сложнее работы со значениями, особенно в API: помимо владения необходимо беспокоиться о правильности используемых типов (aliasing), времени жизни, изменяемости объектов и т.д.
  • Затраты по производительности при копировании значений часто завышены, поэтому прирост производительности при передаче владения (против простого копирования) в ряде случаев не может оправдать ухудшение читабельности и увеличение сложности кода.
  • API управления владением могут накладывать свои ограничения на модель (порядок) управления памятью.
  • При использовании умных указателей нет чёткого понимания где именно (в коде) будет производится освобождение ресурсов.
  • std::unique_ptr реализует передачу владения через семантику перемещения C++11, которая является новой и может затруднить понимание кода.
  • Распределённое владение с одной стороны позволяет аккуратно управлять владением, с другой стороны может усложнить архитектуру системы.
  • Распределённое владение требует операций учёта во время выполнения, это может отразиться на производительности.
  • В ряде случаев (например, создании циклических ссылок) объекты с распределённым владением никогда не удалятся.
  • Умные указатели не всегда могут заменить обычные указатели.

Если необходима работа с динамической памятью, то предпочтительно чтобы код, выделяющий память, ею же и владел. Если другой код хочет получить доступ к этой памяти, то можно передать копию, указатель или ссылку (и всё это без передачи владения). Предпочтительно использовать std::unique_ptr для явной передачи владения. Например:

std::unique_ptr<Foo> FooFactory();
void FooConsumer(std::unique_ptr<Foo> ptr);

Без существенной причины не проектируйте (не используйте) код с распределённым владением. Как вариант, это может быть желание избежать “тяжелой” операции копирования, однако обязательно убедитесь, что выигрыш будет существенным и разделяемый объект – неизменяемый (т.е. std::shared_ptr<const Foo>). Если же требуется именно распределённое владение, то используйте std::shared_ptr.

Никогда не используйте std::auto_ptr. В качестве замены есть std::unique_ptr.

cpplint

Для проверки кода на ошибки стиля используйте cpplint.py.

cpplint.py – утилита, которая читает файл с кодом и определяет многие ошибки в стиле. Конечно, она не идеальна, иногда выдаёт ложно-положительные и ложно-отрицательные ошибки, однако это всё равно полезная утилита. Ложно-положительные ошибки можно исключить, если вставлять // NOLINT в конце строчки кода или // NOLINTNEXTLINE на предыдущей строке.

Иногда в проекте есть инструкция, откуда брать и как пользоваться cpplint.py. Если в вашем проекте такой нет, то можно просто скачать cpplint.py.

Ещё возможности C++

Rvalue-ссылки

Используйте rvalue-ссылки:

  • Для объявления конструкторов перемещения и операторов перемещения.
  • Для объявления перегружаемых функций с const& и && аргументами если это обеспечит значительное улучшение производительности против передачи по значению или если пишется код с небольшими накладными расходами или поддержкой произвольных типов. Избегайте увеличения количества перегружаемых функций (обычно бывает, когда комбинируются типы для несколько параметров).
  • Для поддержки ‘perfect forwarding’ в ‘универсальном’ коде.

Rvalue-ссылка является ссылочным типом, привязанным к временному объекту. По синтаксису похожа на обычную ссылку. Например, void f(std::string&& s); объявляет функцию с аргументом rvalue-ссылка на std::string.

Когда суффикс ‘&&’ (без дополнительных квалификаторов) используется с шаблонным аргументом функции, то применяются специальные правила определения типа аргумента. И такая ссылка имеет название forwarding reference.

  • Определение конструктора перемещения (принимающего rvalue-ссылку на тип класса) даёт возможность переместить (move) класс вместо его копирования. Например, если v1 это std::vector<std::string>, то код auto v2(std::move(v1)) скорее всего выполнит несколько операций с указателями вместо копирования большого объёма данных. И в большинстве случаев это приведёт к существенному увеличению производительности кода.
  • Rvalue-ссылки позволяют реализовать типы, которые можно перемещать, а не копировать. Это полезно для типов, которые нельзя копировать, но которые хочется передавать в функцию как аргумент, хранить в контейнере и т.д.
  • Функция std::move необходима для эффективного использования некоторых типов стандартной библиотеки, таких как std::unique_ptr.
  • Forwarding references, использующие объявление rvalue-ссылки, позволяют написать единую обёртку, перемещающую аргумент в другую функцию. И это одинаково работает вне зависимости от того, временный объект или нет, константный он или не очень. Это и называется ‘perfect forwarding’.

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

Вы можете использовать rvalue-ссылки чтобы объявить конструктор перемещения и перемещающий оператор присваивания (как описано в Копируемые и перемещаемые типы). Также можно почитать “C++ Primer” для более подробной информации о семантике перемещения и std::move.

Вы можете использовать rvalue-ссылки чтобы объявить пары перегружаемых функций, одна с Foo&&, другая с const Foo&. Обычно программисты предпочитают передавать аргументы по значению. Однако использование пары функций может дать лучшую производительность, или позволит в обобщённом коде поддержать большое количество типов. И не забывайте: если ради производительности пишется более сложный код, проверьте и убедитесь, что это действительно помогает.

Вы можете использовать ‘forwarding references’ вместе с std::forward, чтобы реализовать ‘perfect forwarding’.

Дружественные сущности

В ряде случаев допустимо использовать классы и функции как friend.

Дружественные типы обычно определяются в том же файле, поэтому нет необходимости открывать другой файл, чтобы разобраться с использованием закрытых членов класса. Обычное использование friend: когда класс FooBuilder объявляется дружественным (friend) классу Foo, так что FooBuilder может корректно настроить внутреннее состояние Foo без необходимости открывать это состояние всем остальным. В ряде случаев удобно сделать класс unit-тестов дружественным исходному классу.

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

Исключения (программные)

Мы НЕ используем исключения C++.

  • Исключения позволяют обрабатывать ситуации типа “это невозможно” не в месте возникновения ошибки, а на более верхнем уровне, позже. И всё это без копания в исходниках и составления таблиц с кодами ошибок.
  • Исключения используются в большинстве современных языков программирования и их использование в C++ позволяет писать код, концептуально схожий с Python, Java и др.
  • Некоторые библиотеки C++ используют исключения в своей работе и отказ от них может существенно усложнить интеграцию с этими библиотеками.
  • Исключения являются единственным способом проинформировать о проблемах в конструкторе класса. Конечно, это можно обойти, используя фабричные методы или метод Init(), однако это потребует либо дополнительного выделения памяти или, соответственно, обработку специального “невалидного” состояния.
  • Исключения очень удобны при использовании в тестировании (в тестовых фреймворках).

  • Если используется throw в функции, то вы должны проверить всех, кто эту функцию вызывает. Должна быть базовая гарантия безопасности при исключениях. Или же код никогда не ловит исключения и тогда программа может внезапно завершиться. Например есть код, где f() вызывает g(), который вызывает h(). Если h выбрасывает исключение, которое отлавливает f, то g нужно писать аккуратно, иначе могут быть утечки ресурсов и т.д.
  • Обычно использование исключений усложняет отслеживание последовательности выполнения кода. Например, функции могут завершаться в неожиданных местах. Это затрудняет поддержку кода и его отладку. Вы можете улучшить ситуацию, следуя (своим) правилам когда и где можно использовать исключения. Однако, очень желательно, чтобы и другие разработчики знали об этих правилах.
  • Безопасное использование исключений требует использования дополнительных принципов кодирования, например, RAII. И их количество (принципов кодирования) может быть значительным. Например, чтобы разработчик не разбирался в тонкостях всей цепочки вызовов неизвестного ему кода, желательно выделить код сохранения данных в хранилище в отдельную фазу-фиксацию. Такое выделение может нести как плюсы, так и минусы (э-э-э, корпоративная политика смешивания кода в одну кучу для удобства обсфуркации?). В любом случае, использование исключений уменьшает количество доступных вариантов.
  • Использование исключений ведёт к распуханию бинарного файла программы, увеличивает время компиляции (иногда только чуть-чуть) и вообще может привести к проблемам с адресным пространством.
  • Само наличие исключений может провоцировать разработчиков выбрасывать их по поводу и без повода, даже когда это не нужно или может привести к сложностям в обработке. Например, если пользователь вводит неподходящий текст, это не должно приводить к выбрасыванию исключения. И вообще, если всё это расписывать, то никакого документа не хватит!

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

Т.к. большинство C++ кода в Google не использует исключений, то очень проблематично будет внедрять новый код, который будет генерировать исключения. Существующий код в Google не может корректно работать с исключениями, поэтому цена внедрения исключений намного выше, чем реализация любого нового проекта. Переписывание существующего кода под обработку исключений – это будет очень медленный процесс, с большим количеством ошибок. Поэтому лучше использовать альтернативу в виде возврата кода ошибки и assert-ов: это не так сложно.

Этот запрет также распространяется на добавленные в C++11 возможности, такие как std::exception_ptr и std::nested_exception.

Однако, для кода под Windows есть послабления.

noexcept

Указывайте noexcept, если это корректно и будет полезно.

Спецификатор noexcept используется для указания, что функция не будет выбрасывать исключения. Если же функция с таким спецификатором всё же выбросит исключение, то произойдёт крэш программы через std::terminate.

Также есть оператор noexcept. Он выполняет проверку: объявлено ли выражение как “не выбрасывающее исключений”. Проверка проводится на этапе компиляции.

  • Спецификация конструктора перемещения как noexcept может улучшить производительность в ряде случаев, например std::vector<T>::resize() скорее переместит объект нежели скопирует его, если конструктор перемещения для типа T заявлен как noexcept.
  • Указание noexcept для функций может разрешить дополнительную оптимизацию, например компилятор может не генерировать код по раскрутке стека. Конечно, это имеет смысл, если в целом исключения разрешены.

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

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

Используйте безусловный noexcept, если исключения полностью запрещены (т.е. в типовом проекте C++ в Google). В ином случае, используйте спецификатор noexcept с условиями (желательно простыми), которые становятся false в тех редких случаях, когда функция может всё-таки выбросить исключение. Эти тесты могут пользоваться проверками на характеристики типов (например, std::is_nothrow_move_constructible для объектов создаваемых через конструктор перемещения) или аллокаторов (например, absl::default_allocator_is_nothrow). Отметим, что наиболее частая причина исключений – невозможность выделения памяти (и да, мы верим, что это не относится к конструкторам перемещения – они не должны выбрасывать исключений из-за ошибок выделения памяти) и есть много приложений, для которых эта ситуация означает фатальную ошибку, которую даже не имеет смысла обрабатывать. И даже в других, потенциально ошибочных, ситуациях рекомендуется делать упор на простоту интерфейса, нежели на поддержку всех сценариев обработки ошибок: например, вместо написания накрученного noexcept с зависимостью от внешней хэш-функции (выбрасывает она исключения или нет), можно просто задокументировать, что разрабатываемый компонент не поддерживает хэш-функции, которые выбрасывают исключения. И, после этого, использовать noexcept без всяких дополнительных условий.

Информация о типе во время выполнения (RTTI)

Не используйте информацию о типе во время выполнения (RTTI).

RTTI позволяет запросить информацию о C++ классе объекта во время выполнения. Делается через typeid или dynamic_cast.

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

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

RTTI бывает полезно при работе с абстрактными объектами. Например:

bool Base::Equal(Base* other) = 0;
bool Derived::Equal(Base* other) {
  Derived* that = dynamic_cast<Derived*>(other);
  if (that == nullptr)
    return false;
  ...
}

Часто сам запрос типа объекта в процессе выполнения означает проблемы с дизайном приложения, показывает наличие изъянов в иерархии классов.

Бесконтрольное использование RTTI усложняет поддержку кода. Это может привести в развесистым условиям, которые зависят от типа объекта, которые рассыпаны по всему коду. И которые придётся досконально изучать если будет необходимость что-то изменить в этом коде.

Использование RTTI может легко привести к злоупотреблениям, поэтому будьте аккуратны. Старайтесь ограничить использование RTTI только юнит-тестами. Рекомендуется отказаться от RTTI в новом коде. Если же требуется написать код, который ведёт себя по разному в зависимости от типа объекта, возможно следующие альтернативы будут более подходящими:

  • Виртуальные методы. Это предпочтительный способ для выполнения различного кода в зависимости от типа объекта. И вся работа (код) делается в самом объекте.
  • Если код должен находится вне объектов, то можно использовать подходы, аналогичные двойной диспетчеризации, например шаблон проектирования Посетитель/Visitor. Это позволит внешнему коду самому определять тип объектов (используя систему типов).

Когда логика программы гарантирует, что полученный указатель на базовый класс фактически есть указатель на определённый производный класс, тогда можно свободно использовать dynamic_cast. Правда, в этом случае лучше использовать static_cast.

Большое количество условий, основанных на типе объекта, есть показатель явных проблем в коде.

if (typeid(*data) == typeid(D1)) {
  ...
} else if (typeid(*data) == typeid(D2)) {
  ...
} else if (typeid(*data) == typeid(D3)) {
...

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

И пожалуйста, не изобретайте собственный велосипед на замену RTTI. Аргументы против собственного решения будут такие же (см. выше), да и разбираться в чужих велосипедах обычно сложнее.

Приведение типов / Casting

Рекомендуется использовать приведение типов в C++-стиле: static_cast<float>(double_value). Также можно использовать инициализацию значением в скобках для преобразования арифметических типов: int64 y = int64{1} << 42. Избегайте конструкций вида int y = (int)x или int y = int(x) (хотя последний вариант допустим при вызове конструктора класса).

В C++ приведение типов расширяется по сравнению с чистым C путём добавления операций приведения.

Основная проблема с приведением типов в чистом C – неоднозначность операции. Например, одинаково записанная операция: (int)3.5 и (int)"hello" сильно отличается по смыслу. Инициализация в скобках и операции в стиле C++ часто помогают избежать такой неоднозначности. Дополнительная плюшка: операции приведения в стиле С++ легче искать по коду.

C++-стиль довольно громоздкий.

Избегайте использования приведения типов в стиле чистого C. Вместо этого используйте стиль C++ когда требуется явное преобразование типов.

  • Используйте инициализацию в скобках для преобразования арифметических типов (int64{x}). Это самый безопасный способ, т.к. в других случаях (при потере информации при конвертации) код может не скомпилироваться. Плюс лаконичный синтаксис.
  • Используйте static_cast как эквивалент преобразований в C-стиле или когда необходимо преобразовать указатель на дочерний класс в указатель на базовый и обратно (указатель на базовый класс преобразовать в указатель на дочерний (и желательно, чтобы сам объект был экземпляром дочернего класса)).
  • Используйте const_cast чтобы убрать квалификатор const (см. const).
  • Используйте reinterpret_cast чтобы небезопасно преобразовать указатели к целым числам или указателям другого типа. Используйте эту операцию только если вы знаете, что делаете и понимаете проблемы с выравниванием. Также, можете использовать в таких случаях absl::bit_cast.
  • Используйте absl::bit_cast для приведения “сырых” битов в другой тип такого же размера (например, если требуется интерпретировать double как int64).

Также может быть полезным раздел RTTI с описанием dynamic_cast.

Потоки / Streams

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

Потоки являются стандартной абстракцией для ввода/вывода в C++, (см. стандартный заголовочный файл <iostream>). Они часто используются в коде в Google, особенно для отладочного логирования и диагностики.

Операторы << и >> реализуют форматированный ввод/вывод, они просты в понимании, портабельны, расширяемы и повторно используемы. Противоположность к ним – printf, который даже не поддерживает работу с std::string. Кроме того не работает с пользовательскими типа и с портабельностью там проблемы. Кроме того, printf вынуждает выбирать среди похожих версий одной функции и ориентироваться в десятках форматных символах.

Потоки обеспечивают хорошую поддержку консольного ввода/вывода через std::cin, std::cout, std::cerr и std::clog. Функции из C API тоже хорошо работают, однако могут требовать вручную буферизировать ввод.

  • Форматирование потоков можно настроить через манипуляторы, изменяющие состояние потока. Учтите, применение манипуляторов сохраняется во времени и, как результат, поведение кода зависит от истории использования потока (или необходимо будет всё восстанавливать в известное состояние после каждого случая возможного применения манипуляторов). Кроме того, пользовательский код может (помимо модификации встроенных параметров) создавать собственные манипуляторы, добавлять переменные состояния и изменять поведение через функционал register_callback.
  • Проблематично полностью контролировать вывод потока: смена настроек (см. выше), вывод мешанины из параметров и собственно данных, использование перегрузки операторов (и компилятор может выбрать не ту перегрузку, которая предполагалась) – всё это не добавляет управляемости.
  • Формирование вывода посредством вызова цепочки операторов << затрудняет локализацию, т.к. при этом жёстко, прямо в коде, фиксируется порядок слов. И средства поддержки локализации лишаются ценности.
  • Потоковый API может быть сложным, с тонкостями. И программисты должны уметь с ним работать, чтобы код был эффективным.
  • Выбор правильной версии оператора << из большого количества аналогичных является для компилятора затратным. При частом их использовании в большой кодовой базе, время парсинга и анализа может отъедать до 20% времени.

Используйте потоки только если они являются наилучшим решением. Обычно это варианты ввода/вывода в человекочитаемый формат, предназначенный для разработчиков, а не для конечного пользователя. Также не забывайте, что в проекте уже могут быть устоявшиеся методы ввода/вывода – старайтесь использовать их. В частности, библиотеки для логирования и диагностического вывода обычно предпочтительнее нежели std::cerr или std::clog. И вместо std::stringstream лучше использовать absl/strings или их эквивалент.

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

Если потоки всё же используются, старайтесь избегать API работы с состояниями, кроме состояния ошибки. Т.е. не используйте imbue(), xalloc() и register_callback(). Рекомендуется использовать явные функции форматирования (см. absl/strings) вместо манипуляторов или флагов форматирования для таких вещей как смена основания системы счисления, точности или набивка нулями до нужного размера чисел.

Перегружайте оператор << только для типа-значения с тем, чтобы оператор выводил человеко-читаемое представление. Не выводите в поток детали реализации или внутренние переменные. Если же требуется отладочная печать внутреннего состояния, то используйте обычные функции-методы (например, метод класса DebugString() – подходящий вариант).

Преинкремент и предекремент

Используйте префиксные формы (++i) инкремента и декремента над итераторами и другими шаблонными объектами.

Когда переменная инкрементируется (++i, i++) или декрементируется (--i, i--), а возвращаемое значение не используется, то необходимо чётко понимать: использовать префиксную форму (++i, --i) или постфиксную (i++, i--).

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

Традиционно раньше в разработке (особенно на языке C) использовалась постфиксная форма, особенно для циклов for. Иногда программистам легче читать код с постинкрементом, т.к. “субъект” (i) стоит перед “действием” (++), как в английском предложении.

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

Использование const

В API используйте const когда это имеет смысл. В ряде случаев constexpr будет лучшей альтернативой const.

При объявлении переменных или параметров вначале может быть указано const, чтобы показать что переменные не изменяются (например, const int foo). Функции класса могут быть с квалификатором const, чтобы показать, что эта функция не изменяет состояние членов класса (например, class Foo { int Bar(char c) const; };).

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

Использование const оно “заразное”: если передаётся const переменная в функцию, то она должна в прототипе иметь указание на const (или придётся делать const_cast). И это может быть проблемой при вызове библиотечных функций.

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

  • Если функция не меняет аргумент, переданный по ссылке или указателю, то он должен быть ссылкой на константу (const T&), либо указателем на константу (const T*).
  • Если параметр передаётся по значению, то использование const не даёт никакого эффекта. Поэтому не рекомендуется объявлять константный параметр. См. также TotW #109.
  • Старайтесь объявлять члены класса константными (const) если они не изменяют логическое состояние объекта (и не дают возможности другим что-то менять, например через возврат не-константной ссылки). В противном случае эти методы могут быть небезопасными в многопоточном окружении.

Использование const для локальных переменных отдаётся на усмотрение программиста: можно использовать, можно – нет.

Все const операции класса должны работать корректно при одновременном вызове нескольких функций. Если это не выполняется, то класс должен быть явно описан как “потоко-не-безопасный”.

Местоположение const

Иногда используется форма int const *foo вместо const int* foo. Обосновывается это тем, что первая форма более логична: const следует за описываемым объектом. Однако, такая “логичность” имеет мало смысла (и обычно не применяется в коде с несколькими вложенными маркерами “указатель”), т.к. чаще всего есть только один const для базового значения. В таком случае нет необходимости специально заботиться о логичности. Размещение же const вначале делает код более читабельным и согласованным с английским языком: прилагательное (const) стоит перед существительным (int).

Так что расположение const вначале является предпочтительным. Однако, это не жёсткое условие и если остальной код в проекте использует другой порядок – следуйте за кодом!

Использование constexpr

Используйте constexpr для определения констант или чтобы сделать константную инициализацию.

Переменные можно объявлять как constexpr для указания на константу, значение которой определяется во время компиляции или линковки. Также можно объявлять функции и конструкторы как constexpr, чтобы их можно было использовать для определения переменной с constexpr.

constexpr позволяет определять выражения с плавающей запятой (помимо литералов), использовать константы для пользовательских типов и вызовов функций.

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

constexpr позволяет определить неизменяемые части интерфейса. Используйте constexpr чтобы определить константы и функции для задания им значений. Не используйте constexpr, если это потребует усложнения кода. Не используйте constexpr, чтобы сделать код “встраиваемым” (inlining).

Целочисленные типы

Среди встроенных целочисленных типов C++ необходимо использовать int. Если в программе требуется переменная другого размера, то используйте целочисленные типы фиксированной длины из <stdint.h>, такие как int16_t. Если переменной нужно хранить значения, равные или превышающие 2^31 (2GiB), используйте 64-битный тип int64_t. При оценке размера не забудьте, что в int должен укладываться не только результат, но и промежуточные значения при вычислениях. И, если сомневаетесь, используйте тип подлиннее.

C++ не уточняет размер целочисленных типов, таких как int. Обычно считается, что short содержит 16 битов, int – 32, long – 32 и long long содержит 64 бита.

Унификация в коде.

Размеры целочисленных типов в C++ могут изменяться в зависимости от компилятора и архитектуры.

В <cstdint> определяются различные типы: int16_t, uint32_t, int64_t и т.д. Если требуются типы фиксированного размера, то не используйте short, unsigned long long и им подобные. Из целочисленных типов языка C можно использовать только int. Также, в соответствующих случаях, используйте size_t и ptrdiff_t.

Тип int используется очень часто, особенно для небольших значений, например как счётчики в циклах. Можете считать, что int содержит минимум 32 бита (но не больше). Если требуется 64 битный целочисленный тип, то используйте int64_t или uint64_t.

Для типа который может хранить “большие значения” используйте int64_t.

Старайтесь не использовать беззнаковые числа (например, uint32_t). Допустимое применение беззнаковых чисел это использование битовых представлений или использование переполнения (по модулю 2^N) в расчётах. Отметим, что также не рекомендуется использовать беззнаковый тип чтобы указать на отсутствие отрицательных значений: в этом случае используйте assert-ы.

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

Будьте внимательны при конвертировании целочисленных типов. Может появится неопределённое поведение (UB), ведущее к багам безопасности и другим проблемам.

Беззнаковые целые числа

Беззнаковые целые числа отлично подходят для работы с битовыми полями и модульной арифметики. Так сложилось, что стандарт C++ использует беззнаковые числа и для возврата размера контейнеров (хотя многие члены организации по стандартизации и считают это ошибкой; в любом случае, сейчас это уже не изменить). Ситуация, что беззнаковая арифметика по поведению является модульной (заворачивание значений при переполнении) и отличается от обычной (знаковой), не позволяет компилятору диагностировать большое количество ошибок. Фактически, такое поведение затрудняет оптимизацию.

С другой стороны, совместное использование беззнаковых и знаковых целых чисел создаёт ещё больше проблем. Лучшее решение: старайтесь использовать итераторы вместо указателей и явных размеров; не мешайте беззнаковые числа вместе со знаковыми; избегайте беззнаковых чисел (кроме работы с битовыми полями и для модульной арифметики); не используйте беззнаковые числа только чтобы показать, что переменная неотрицательная.

Совместимость с 64-бит

Написанный код должен быть совместим как с 64-битной, так и с 32-битной архитектурой. Особое внимание обращайте на печать в консоль, операции сравнения и выравнивание структур.

  • Форматные символы printf() для некоторых целочисленных типов основываются на макроопределениях (например, PRI из <cinttypes>). И такой подход является непрактичным, нежелательным и т.д. Поэтому очень желательно не использовать функцию printf (и подобные) в своём коде, или даже переписать существующий код. Вместо неё можно использовать библиотеки, поддерживающие безопасное форматирование числовых значений, такие как StrCat или Substitute для простых преобразований, или std::ostream.

    К сожалению, макросы PRI являются единственным переносимым способом указать формат для типов данных с задаваемым размером (int64_t, uint64_t, int32_t, uint32_t и т.д.). По возможности не передавайте аргументы таких типов в функции, основанные на printf. Исключение составляют типы, для которых есть свой выделенный модификатор длины, например size_t (z), ptrdiff_t (t) и maxint_t (j).

  • Помните, что sizeof(void*) != sizeof(int). Используйте intptr_t в случае, если требуется целочисленный тип размером, равным размеру указателя.
  • Будьте аккуратны с выравниванием структур, особенно тех что записываются на диск. Обычно классы и структуры, в которых есть член типа int64_t или uint64_t, по умолчанию выравниваются на границу 8 байт в 64-битных системах. Если в коде есть подобные структуры, они сохраняются на диске и используются 32-битным и 64-битным кодом, то обязательно проверьте, что структуры упаковываются одинаково на обеих архитектурах. Большинство компиляторов позволяют задать выравнивание структур. В gcc можно использовать __attribute__((packed)). В MSVC – #pragma pack() и __declspec(align()).
  • Используйте инициализацию в фигурных скобках если требуется создать 64-битную константу. Например:

    int64_t my_value{0x123456789};
    uint64_t my_mask{3ULL << 48};
    

Макросы препроцессора

Избегайте определения макросов, особенно в заголовочных файлах. Вместо этого используйте встраиваемые функции, перечисления или переменные-константы. Если используете макросы, то в имени используйте префикс – название проекта. Не используйте макросы, чтобы переопределить или дополнить C++ API.

Использование макросов подразумевает, что программист видит один код, а компилятор – другой. Это может вызвать неожиданные последствия, особенно если макросы глобальные.

Ситуация может усугубиться, когда макросы используются для переопределения С++ или другого публичного API. При любых ошибках в использовании API потребуется разбираться в логике макросов; увеличивается время разбора кода инструментами рефакторинга или анализаторами. Как результат, использование макросов в таких случаях запрещено. Например, откажитесь от подобного кода:

class WOMBAT_TYPE(Foo) {
  // ...

 public:
  EXPAND_PUBLIC_WOMBAT_API(Foo)

  EXPAND_WOMBAT_COMPARISONS(Foo, ==, <)
};

К счастью, в C++ зависимость от макросов поменьше, чем в C. Вместо макросов для высокопроизводительного кода можно использовать встраиваемые функции. Для хранения констант есть const переменные. Чтобы для удобства “укоротить” длинное имя переменной используйте ссылки. Вместо применения макросов для условной компиляции кода используйте … лучше не используйте условную компиляцию (конечно же это не касается защиты от повторного включения заголовочных файлов через #define). Тем более условная компиляция затрудняет тестирование кода.

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

Следующие правила позволят избежать ряда проблем с макросами. По возможности следуйте им:

  • Не определяйте макросы в заголовочных (.h) файлах.
  • Определяйте (#define) макросы по возможности ближе к месту первого использования. И когда макросы уже не используются, то делайте #undef.
  • Не делайте #undef существующих макросов с последующим их переопределением своей версией. Вместо этого лучше придумайте для своих макросов уникальное имя и используйте его.
  • Постарайтесь не использовать макросы, которые раскрываются в большие и неустойчивые конструкции C++. По крайней мере, документируйте такое поведение.
  • Старайтесь не использовать ## для генерации имени функции/класса/переменной.

Настоятельно не рекомендуется экспортировать макросы из заголовочных файлов (т.е. определять макрос и не делать #undef его в конце заголовочного файла). Если макрос экспортируется из заголовочного файла, то он должен иметь глобальное уникальное имя. Как вариант, добавьте префикс с именем пространства имён проекта (заглавными буквами).

0 и nullptr/NULL

Используйте nullptr для указателей и '\0' для char-ов (не используйте 0 для этих целей).

Для указателей (адресов) используйте nullptr, это улучшает безопасность типов.

Для проектов C++03 лучше используйте NULL, а не 0. Хотя оба варианта эквивалентны, NULL обычно ассоциируется с указателями. Также некоторые C++ компиляторы могут определять NULL специальным образом и выдавать более адекватные предупреждения компиляции. Никогда не используйте NULL как числовое значение (целочисленное или с плавающей запятой).

Используйте '\0' в качестве символа конца строки (пустого символа). Это улучшает читабельность кода.

sizeof

Рекомендуется использовать sizeof(переменная) вместо sizeof(тип).

Используйте sizeof(переменная) если необходим размер определённой переменной. sizeof(переменная) будет возвращать корректное значение даже если в дальнейшем изменится тип переменной. sizeof(тип) можно использовать, когда код не работает с конкретной переменной, например в случае форматирования/разбора данных, где соответствующий тип C++ не подходит.

struct data;
memset(&data, 0, sizeof(data));
memset(&data, 0, sizeof(Struct)); // Плохо
if (raw_size < sizeof(int)) {
  LOG(ERROR) << "compressed record not big enough for count: " << raw_size;
  return false;
}

Вывод типов

Используйте вывод типов только если это сделает код более читабельным или более безопасным. Не используйте его только из-за неудобства написания полного типа.

Есть ряд ситуаций, когда типы в C++ коде могут (или даже необходимо) быть выведены компилятором и это более предпочтительно, чем явно их прописывать:

вывод типов аргументов шаблонной функции
Шаблонная функция может вызываться без указания явных шаблонных типов. Компилятор выводит эти типы из аргументов функции:
template <typename T>
void f(T t);

f(0);  // Вызывается f<int>(0)
переменная с auto типом
Декларация переменной может использовать auto вместо типа. Компилятор выводит тип из выражения инициализации, следуя правилам, аналогичным для шаблонной функции (во всяком случае, пока не используются фигурные скобки вместо круглых).
auto a = 42;  // a типа int
auto& b = a;  // b типа int&
auto c = b;   // c типа int
auto d{42};   // d типа int, а не std::initializer_list<int>
auto может использоваться совместно с const, или в составе указателя или ссылки. Однако, auto не может использоваться как аргумент шаблона. Изредка можно увидеть использование decltype(auto) вместо auto и в этом случае выводимый тип является результатом применения decltype к переданному выражению инициализации (прим.: и, например, сохранить квалификаторы ссылки).
вывод типа возвращаемого значения функции
autodecltype(auto)) можно использовать для указания возвращаемого значения функции. Компилятор выводит тип возвращаемого значения из выражения return в теле функции, следуя тем же правилам, что и при объявлении переменной:
auto f() { return 0; }  // Возвращаемый f тип - int
Возвращаемый тип лямбды может быть выведен аналогичным способом (хотя это делается при отсутствии возвращаемого типа, а не в случае использования auto). Синтаксис указания возвращаемого типа в конце для функций также использует auto, но это не вывод типа, скорее альтернативный синтаксис для явно указанного возвращаемого типа.
общие (generic) лямбды
В описании лямбды можно использовать auto в качестве одного или нескольких типов параметров. Как результат, оператор вызова лямбды будет шаблонной функцией вместо обычной, с отдельными параметрами шаблона по одному на каждый auto:
// Сортируем `vec` по возрастанию
std::sort(vec.begin(), vec.end(), [](auto lhs, auto rhs) { return lhs > rhs; });
инициализация захватываемых переменных лямбды
В лямбде в секции захвата можно явно прописать новые переменные, инициализированные значением:
[x = 42, y = "foo"] { ... }  // тип x - int, y - const char*
Синтаксис не позволяет указать тип новой переменной, он выводится аналогично auto переменным.
вывод аргументов шаблонного класса
См. соответствующий раздел.
структурная привязка
При объявлении кортежей, структур или массивов с использованием auto можно указать имена отдельных элементов вместо имени полного объекта. Эти имена называются “структурная привязка”, а декларация – соответственно “декларация структурной привязки”. Синтаксис не позволяет задать тип ни полного объекта, ни отдельных имён:
auto [iter, success] = my_map.insert({key, value});
if (!success) {
  iter->second = value;
}
auto можно использовать с квалификаторами const, & и &&. Отметим, что эти квалификаторы формально применяются к анонимному кортежу/структуре/массиву, а не к отдельным привязкам. Правила определения конечного типа привязок довольно запутанные, однако в большинстве случаев всё довольно логично. Можно только отметить, что тип привязки обычно не может быть ссылочным, даже если в декларации указана ссылка (хотя поведение всё равно может быть как у ссылки).

В приведённом выше описании не указаны многие детали, для дополнительной информации используйте приведённые ссылки.

  • Название типов в C++ может быть длинным и громоздким, особенно при использовании шаблонов и пространств имён.
  • Когда название типа C++ повторяется несколько раз внутри небольшого куска кода или декларации, это повторение не улучшает читабельность.
  • В ряде случаев вывод типов безопаснее, т.к. позволяет избежать случайных копирований или преобразований типов.

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

auto foo = x.add_foo(); // Плохо. Что есть foo?
auto i = y.Find(key);

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

Необходимо разбираться, выдаётся ли ссылка при выводе типа, производится ли копирование (особенно если оно не предполагалось).

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

Основное правило: используйте вывод типов только если это сделает код более ясным и безопасным. Не используйте вывод типов только чтобы облегчить написание кода. Не забывайте, что читатели кода могут быть в другой команде и не знакомы с этим проектом. Поэтому, хотя явные типы могут считаться понятным, очевидным и избыточными для одних, они могут содержать полезную информацию для других. Например, можно полагать, что возвращаемый тип make_unique<Foo>() очевиден. Однако, в случае MyWidgetFactory() лучше считать по-другому.

Эти принципы применяются для всех видов вывода типов. Однако, существуют тонкости, описанные ниже.

Вывод аргументов шаблонной функции

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

Вывод типов локальных переменных

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

std::unique_ptr<WidgetWithBellsAndWhistles> widget_ptr =
    absl::make_unique<WidgetWithBellsAndWhistles>(arg1, arg2);
absl::flat_hash_map<std::string,
                    std::unique_ptr<WidgetWithBellsAndWhistles>>::const_iterator
    it = my_map_.find(key);
std::array<int, 0> numbers = {4, 8, 15, 16, 23, 42};
auto widget_ptr = absl::make_unique<WidgetWithBellsAndWhistles>(arg1, arg2);
auto it = my_map_.find(key);
std::array numbers = {4, 8, 15, 16, 23, 42};

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

auto it = my_map_.find(key);
if (it != my_map_.end()) {
  WidgetWithBellsAndWhistles& widget = *it->second;
  // Do stuff with `widget`
}

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

Не используйте decltype(auto) при наличии более простых альтернатив, т.к. результат использования не всегда легко предсказуем.

Вывод типа возвращаемого значения

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

Вывод типов параметров

Параметры с auto в ламбдах должны использовать с осторожностью, т.к. реальный тип определяется кодом лямбды, а не её объявлением. И, конечно, явное указание типа обычно более понятно, за исключением случаев, когда лямбда определяется рядом с местом её использования (можно одновременно видеть и определение лямбды и её вызов) или лямбда передаётся в настолько известный интерфейс, что используемые аргументы очевидны (например, см. вызов std::sort выше).

Инициализация переменных захвата ламбды

При инициализации переменных захвата предпочтительны специальные рекомендации, которые в целом подменяют общие правила для использования вывода типов.

Структурные привязки

В отличие от других форм вывода типов, структурные привязки могут дать дополнительную информацию за счёт правильного именования элементов полного объекта. Т.е. декларация структурной привязки может улучшить читабельность кода по сравнению с использованием явного типа (даже если auto не рекомендуется). Структурные привязки хорошо подходят при работе с парами или кортежами (см. пример использования insert выше), потому что в последних нет “говорящих” названий полей. С другой стороны, в целом не рекомендуется использовать пары и кортежи, пока внешний API, наподобие insert, явно этого не потребует.

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

auto [/*field_name1=*/ bound_name1, /*field_name2=*/ bound_name2] = ...

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

Вывод аргументов шаблонного класса

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

Вывод аргументов шаблонного класса (CTAD) проявляется, когда переменная декларируется с типом шаблона, но без указания аргументов (даже без угловых скобок):

std::array a = {1, 2, 3};  // Тип `a`: std::array<int, 3>

Компилятор выводит аргументы из выражения инициализации, используя “гайд” для шаблонов, который может быть явный и неявный.

Явный гайд походит на декларацию функции с возвращаемым типом в конце, только без auto вначале, и имя функции есть имя шаблона. Например, ранее приведённый пример опирается на следующий гайд для std::array:

namespace std {
template <class T, class... U>
array(T, U...) -> std::array<T, 1 + sizeof...(U)>;
}

Конструкторы в основном определении шаблона (т.е. не в специализации) также неявно определяют “гайд”.

Когда объявляется переменная, использующая CTAD, компилятор выбирает “гайд” на основе правил выбора (разрешения) перегруженного конструктора, и возвращаемый гайдом тип становится типом переменной.

CTAD иногда может уменьшить количество формального кода.

Неявные “гайды”, получаемые из конструкторов, могут реализовывать нежелательное или даже неправильное поведение. Эта проблема может часто проявляться для конструкторов, написанных до появления CTAD в C++17, т.к. авторы кода просто не знали о тех проблемах, которые вызовет CTAD. И далее, добавление явных “гайдов” для исправления проблемы может поломать любой существующий код, который использует неявные гайды.

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

Не используйте CTAD на шаблонных классах, пока не будет поддержки механизма и обеспечен хотя бы один явный “гайд” (предполагается, что в пространстве имён std всё поддерживается). Желательно, чтобы недопустимое использование CTAD приводило к предупреждениями компилятора.

В любом случае, использование CTAD должно следовать общим правилам при выводе типов.

Лямбды

Используйте лямбды в подходящих случаях. Желательно использовать явный захват переменных, если лямбда будет выполнена вне текущей области видимости.

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

std::sort(v.begin(), v.end(), [](int x, int y) {
  return Weight(x) < Weight(y);
});

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

int weight = 3;
int sum = 0;
// Захват `weight` по значению и `sum` по ссылке.
std::for_each(v.begin(), v.end(), [weight, &sum](int x) {
  sum += weight * x;
});

Захват по-умолчанию применяется ко всем переменным, используемым в теле лямбды, в том числе и к this (если используются члены класса):

const std::vector<int> lookup_table = ...;
std::vector<int> indices = ...;
// Захват `lookup_table` по ссылке, сортировка `indices` по значению
// ассоциированных элементов из `lookup_table`.
std::sort(indices.begin(), indices.end(), [&](int a, int b) {
  return lookup_table[a] < lookup_table[b];
});

Захват переменной может быть также с инициализатором, что можно использовать для перемещения (move) переменных по значению или для других случаев, не подпадающих под обычный захват по значению или ссылке:

std::unique_ptr<Foo> foo = ...;
[foo = std::move(foo)] () {
  ...
}

Такой тип захвата (часто называемый init или generalized) нужен скорее не для собственно “захвата” переменных (или даже имён) из текущей области видимости. Этот синтаксис нужен для определения членов объекта лямбды:

[foo = std::vector<int>({1, 2, 3})] () {
  ...
}

И тип такой переменной с инициализатором выводится согласно правилам, аналогичным использованию auto.

  • Лямбды это очень удобный и лаконичный способ определения объектов-функций (например, для передачи в алгоритмы STL), что может улучшить читабельность.
  • Правильное использование захвата по-умолчанию может устранить избыточность и выявить важные исключения при захвате.
  • Лямбды, std::function и std::bind можно использовать совместно как механизм обратного вызова общего назначения. Упрощается написание кода, принимающего/использующего функции как аргументы.

  • Захват переменных может быть источником ошибок с недействительными указателями, особенно если лямбда выходит за текущую область видимости.
  • Захват по умолчанию может вводить в заблуждение, т.к. он не защищает от ошибок недействительных указателей. Захват указателя на объект по значению не приводит к созданию копии самого объекта. При этом часто возникают те же особенности жизненного цикла, что и при захвате по ссылке. Учтите, что ‘this’ также захватывается по значению и это может привести к проблемам, т.к. он часто используется неявно.
  • Захват фактически объявляет новые переменные (вне зависимости от наличия инициализатора), хотя это отличается от обычного синтаксиса C++. В частности, нет указания типа переменной, даже в виде auto (хотя захват с инициализацией может сделать это неявно через приведение типов), и это отличается от типовой декларации.
  • Захват с инициализацией использует вывод типов, и подвержен тем же проблемам, что и auto, плюс сам синтаксис не содержит указания, что происходит вывод типов.
  • В ряде случаев использование лямбд может сильно затруднить понимание кода (очень длинные функции) или потребовать дополнительного контроля.

  • Используйте лямбды в подходящих случаях. Применяйте форматирование, описанное ниже.
  • Рекомендуется явно указывать захват переменных, если лямбда может выйти за текущую область видимости. Например, вместо:
    {
      Foo foo;
      ...
      executor->Schedule([&] { Frobnicate(foo); })
      ...
    }
    // ПЛОХО! При беглом просмотре можно упустить, что лямбда использует
    // ссылку на foo и this (если Frobnicate является членом класса).
    // Если лямбда вызывается после возврата из текущей функции, то
    // это приведёт к проблемам, т.к. foo и другие объекты могут быть
    // уже разрушены.  
    
    лучше написать:
    {
      Foo foo;
      ...
      executor->Schedule([&foo] { Frobnicate(foo); })
      ...
    }
    // ЛУЧШЕ - Компилятор выдаст ошибку, если Frobnicate является методом класса.
    // Также явно указано, что foo захватывается по ссылке.
    
  • Используйте захват по-умолчанию по ссылке ([&]), только если жизненный цикл лямбды явно короче чем у любой переменной.
  • Используйте захват по-умолчанию по значению ([=]), только как средство привязки нескольких переменных для короткой лямбды. Не рекомендуется писать лямбды с объёмным и сложным кодом вместе с захватом по-умолчанию по значению.
  • Используйте захват только существующих переменных из текущей области видимости. Не используйте захват с инициализаторами, чтобы ввести новые имена или подменить существующие. Вместо этого можно объявить обычную новую переменную и передать её в лямбду, или вместо лямбды определить отдельную полноценную функцию.
  • Вопросы по указанию параметров и возвращаемому типу рассмотрены в разделе Вывод типов.

Метапрограммирование на шаблонах

Не используйте сложные/запутанные шаблоны в коде.

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

Метапрограммирование позволяет создавать очень гибкие интерфейсы, у которых высокая производительность и отличная типобезопасность. Например, Google Test, std::tuple, std::function и Boost.Spirit были бы невозможны без таких средств.

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

Метапрограммирование часто приводит к появлению очень скудной и непонятной информации об ошибках компиляции: даже если сам интерфейс простой, то его сложная реализация всё равно проявляется, когда пользователь делает что-то неправильно.

Метапрограммирование усложняет проведение рефакторинга, затрудняя работу инструментов. Во-первых, код шаблона раскрывается в различных контекстах и трудно проверить, что в каждом из них код остаётся корректным. Во-вторых, ряд инструментов работает уже с AST (прим.: абстрактное синтаксическое дерево), которое описывает структуру кода только после раскрытия шаблонов. И в этом случае может быть тяжело обнаруженные проблемы отобразить на исходные конструкции в коде и определить, что требуется переписать.

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

Дважды подумайте перед тем, как использовать метапрограммирование или другие сложные техники на шаблонах: может ли средний программист в команде понять и поддерживать такой код (особенно после переключения с другого проекта); сможет ли не-C++ программист (или другой случайный читатель) понять сообщения об ошибках или отладить выполнение функции, которую он вызывает. Если используются рекурсивное инстанцирование шаблонов, список типов, метафункции, шаблоны выражений или используется SFINAE или трюк с sizeof для разрешения перегрузки функции – скорее всего вы зашли слишком далеко.

Если используется метапрограммирование, то готовьтесь приложить усилия для минимизации сложности, а также её изоляции. По возможности скрывайте код с метапрограммированием внутри реализации, чтобы сделать пользовательские заголовочные файлы более читабельными. Код с метапрограммированием должен быть очень хорошо откомментирован: следует подробно задокументировать, как использовать код и на что будет похож “сгенерированный” результат. Обратите особое внимание на сообщения об ошибках, которые выдаёт компилятор, когда делается что-то ошибочное. Учтите, что сообщения об ошибках являются частью вашего пользовательского интерфейса и код следует доработать так, чтобы сообщения об ошибках были понятными для пользователя и объясняли, что нужно делать для их исправления.

Boost

Используйте только одобренные библиотеки из коллекции Boost.

Boost это популярная коллекция проверенных, бесплатных и открытых библиотек C++.

В целом код Boost является высококачественным, портируемым и во многом дополняется стандартную библиотеку C++, например, в таких областях как свойства типов или улучшенные связыватели (binder).

Некоторые библиотеки Boost поощряют создание кода, который ухудшает читабельность: используется метапрограммирование или другие продвинутые техники на шаблонах, а также чрезмерно “функциональный” стиль.

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

  • Call Traits из boost/call_traits.hpp
  • Compressed Pair из boost/compressed_pair.hpp
  • Boost Graph Library (BGL) из boost/graph, за исключением сериализации (adj_list_serialize.hpp) и параллельных/распределённых алгоритмов и структур данных (boost/graph/parallel/* и boost/graph/distributed/*).
  • Property Map из boost/property_map, за исключением параллельных/распределённых (boost/property_map/parallel/*).
  • Iterator из boost/iterator
  • Часть Polygon, которая работает с построением диаграмм Вороного и не зависит от остальной части Polygon: boost/polygon/voronoi_builder.hpp, boost/polygon/voronoi_diagram.hpp, and boost/polygon/voronoi_geometry_type.hpp
  • Bimap из boost/bimap
  • Statistical Distributions and Functions из boost/math/distributions
  • Special Functions из boost/math/special_functions
  • Root Finding Functions из boost/math/tools
  • Multi-index из boost/multi_index
  • Heap из boost/heap
  • flat-контейнеры библиотеки Container: boost/container/flat_map и boost/container/flat_set
  • Intrusive из boost/intrusive
  • Библиотека boost/sort
  • Preprocessor из boost/preprocessor

В настоящее время прорабатывается вопрос о добавлении других библиотек Boost в этот список, так что он может в будущем дополняться.

std::hash

Не определяйте специализации std::hash.

std::hash<T> это объект-функция, который hash контейнеры C++11 используют для получения хэш-ключа от типа T, если явно не указана другая хэш-функция. Например, std::unordered_map<int, std::string> это hash map, используюущая std::hash<int> для расчёта хэша ключа, тогда как std::unordered_map<int, std::string, MyIntHash> использует для расчёта MyIntHash.

std::hash определена для всех целочисленных чисел и с плавающей запятой, указателей, enum-ов, некоторых типов стандартной библиотеки (например, string и unique_ptr). Также допустимо определить специализацию для своих собственных типов.

std::hash легко использовать, она упрощает код, т.к. нет необходимости вводить свои именования. Специализация std::hash это стандартный способ определения хэша, удобный и понятный и новичкам и внешним командам.

Хорошую специализацию std::hash может быть сложно написать: она может содержать большое количество формального и одинакового кода. Также это может потребовать усилий и по идентификации входных данных и по созданию самого алгоритма. Автор типа данных обычно ответственен за первое, а со вторым чаще всего возникают проблемы, т.к. это требует определённой экспертизы, которой у автора обычно нет (да она и не нужна). При этом плохая реализация алгоритма может внести уязвимости в безопасность кода, например от атак hash flooding.

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

Именно из-за этого std::hash не работает с std::pair и std::tuple, и в язык не добавлена их поддержка.

Можно использовать std::hash с типами, которые поддерживаются “из коробки”, но не добавляйте специализации для своих типов. Если требуется хэш-таблица с ключом, который не поддерживается std::hash, то возможно следует воспользоваться другими (ранними) хэш-контейнерами (например, hash_map); в них используется другой хэш-алгоритм, на который этот запрет не влияет.

Если всё равно требуется стандартный контейнер, то задаёте собственную хэш-функцию для ключа, например:

std::unordered_map<MyKeyType, Value, MyKeyTypeHasher> my_map;

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

Возможно, скоро появится хэш-функция, которая сможет работать с любым типом и использовать новый механизм подстройки, при этом у неё не будет недостатков std::hash.

Ещё возможности C++

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

Настоятельно не рекомендуется использовать следующие возможности C++:

  • Рациональные числа (относящиеся к времени компиляции) (<ratio>), т.к. за интерфейсом может стоять сложный шаблон.
  • Заголовочные файлы <cfenv> и <fenv.h>, т.к. многие компиляторы не поддерживают корректную работу этого функционала.
  • Заголовочный файл <filesystem>, который недостаточно протестирован, и подвержен уязвимостям в безопасности.

Нестандартные расширения

Нестандартные расширения C++ не рекомендуется использовать, пока явно не указано обратное.

Компиляторы поддерживают различные расширения, не являющиеся частью стандартного C++. Например, __attribute__, внутренние (intrinsic) функции (__builtin_prefetch), назначенные инициализаторы (Foo f = {.field = 3}), ассемблерные вставки, __COUNTER__, __PRETTY_FUNCTION__, составные выражения (foo = ({ int x; Bar(&x); x }), массивы переменной длины, alloca() и “оператор Элвисa?:b.

  • Нестандартные расширения могут предоставить полезные возможности, которых нет в стандартном C++. Например, многим назначенные инициализаторы нравятся больше, чем типовые конструкции C++ (конструкторы).
  • Сделать указания компилятору по оптимизации производительности кода можно только используя расширения.

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

Не используйте нестандартные расширения. Можно использовать портируемые обёртки кода, которые реализованы с использованием нестандартных расширений и и (желательно) содержатся в одном заголовочном файле (portability header).

Псевдонимы/Alias

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

Есть несколько способов для создания имён, являющихся псевдонимами для других сущностей:

typedef Foo Bar;
using Bar = Foo;
using other_namespace::Foo;

При написании нового кода рекомендуется использовать using, а не typedef. Это обеспечивает более согласованный синтаксис с остальным C++ кодом и поддерживает работу с шаблонами.

Аналогично другим декларациям, псевдонимы, введённые в заголовочном файле, обычно являются частью публичного API этого файла. Исключения касаются случаев, когда псевдонимы объявлены внутри определения функции, private секции класса или явно отмеченном внутреннем пространстве имён. Такие псевдонимы, а также введённые в .cc файлах являются “деталями реализации” (т.к. клиентский код к ним не обращается) и не подпадают под действие этих правил.

  • Псевдонимы могут улучшить читабельность кода, сокращая длинные и сложные имена.
  • Псевдонимы могут уменьшить дублирование: например, сделав именование типа, часто использующегося в API, в дальнейшем это может облегчить изменение этого типа на другой.

  • Псевдонимы, размещённые в заголовочном файле (где клиентский код может обращаться к ним), увеличивают количество сущностей в этом файле API и его сложность.
  • Клиентский код может полагаться на особенности публичных псевдонимов, и это усложняет внесение изменений.
  • Есть соблазн создавать публичные псевдонимы, которые будут использоваться только во внутренней реализации. Такой подход может влиять на сам API и усложнит его поддержку.
  • Псевдонимы увеличивают риск создания дубликата для другого имени.
  • Псевдонимы могут ухудшить читабельность кода, если хорошо известным сущностям будут давать незнакомые имена.
  • Псевдонимы типов могут создать “ненадёжное” соглашение по API: то ли псевдоним всегда будет идентичным указанному типу (API не изменится, тип можно использовать любым способом); то ли тип у псевдонима может поменяться (тогда рекомендуется использовать только небольшую часть возможностей).

Не вводите псевдонимы в публичный API только для облегчения написания кода в реализации; псевдоним должен прежде всего быть полезен для клиентского кода.

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

Не объявляйте публичные псевдонимы на пространства имён в своём API. (См. также Пространство имён).

Например, следующие псевдонимы описывают способ их использования в клиентском коде:

namespace mynamespace {
// Используется для хранения измерений. DataPoint может меняться с Bar* на другой
// внутренний тип, его следует трактовать как абстрактный указатель.
using DataPoint = foo::Bar*;

// Набор измерений. Добавлен для удобства пользователя.
using TimeSeries = std::unordered_set<DataPoint, std::hash<DataPoint>, DataPointComparator>;
}  // namespace mynamespace

Приведённые ниже псевдонимы не документируют способы использования и часть из них вообще не предназначения для использования в клиентском коде:

namespace mynamespace {
// Плохо: непонятно, как это использовать.
using DataPoint = foo::Bar*;
using std::unordered_set;  // Плохо: это для внутреннего удобства
using std::hash;           // Плохо: это для внутреннего удобства
typedef unordered_set<DataPoint, hash<DataPoint>, DataPointComparator> TimeSeries;
}  // namespace mynamespace

Однако, локальные псевдонимы очень удобны внутри определения функций, private секций классов, внутренних пространств имён и в .cc файлах:

// В .cc файле
using foo::Bar;

Именование

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

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

Общие принципы именования

Используйте имена, который будут понятны даже людям из другой команды.

Имя должно говорить о цели или применимости объекта. Не экономьте на длине имени, лучше более длинное и более понятное (даже новичкам) имя. Поменьше аббревиатур, особенно если они незнакомы вне проекта. Используйте только известные аббревиатуры (Википедия о них знает?). Не сокращайте слова. В целом, длина имени должна соответствовать размеру области видимости. Например, n – подходящее имя внутри функции в 5 строк, однако при описании класса это может быть коротковато.

class MyClass {
 public:
  int CountFooErrors(const std::vector<Foo>& foos) {
    int n = 0;  // Чёткий смысл для небольшой области видимости
    for (const auto& foo : foos) {
      ...
      ++n;
    }
    return n;
  }
  void DoSomethingImportant() {
    std::string fqdn = ...;  // Известная аббревиатура полного доменного имени
  }
 private:
  const int kMaxAllowedConnections = ...;  // Чёткий смысл для контекста
};
class MyClass {
 public:
  int CountFooErrors(const std::vector<Foo>& foos) {
    int total_number_of_foo_errors = 0;  // Слишком подробное имя для короткой функции
    for (int foo_index = 0; foo_index < foos.size(); ++foo_index) {  // Лучше использовать `i`
      ...
      ++total_number_of_foo_errors;
    }
    return total_number_of_foo_errors;
  }
  void DoSomethingImportant() {
    int cstmr_id = ...;  // Сокращённое слово (удалены буквы)
  }
 private:
  const int kNum = ...;  // Для целого класса очень нечёткое имя
};

Отметим, что типовые имена также допустимы: i для итератора или счётчика, T для параметра шаблона.

В дальнейшем при описании правил “word” / “слово” это всё, что пишется на английском без пробелов, в том числе и аббревиатуры. В слове первая буква может быть заглавной (зависит от стиля: “camel case” или “Pascal case”), остальные буквы – строчные. Например, предпочтительно StartRpc(), нежелательно StartRPC().

Параметры шаблона также следуют правилам своих категорий: type names / имена типов для типов, variable names / имена переменных для переменных.

Имена файлов

Имена файлов должны быть записаны только строчными буквами, для разделения можно использовать подчёркивание (_) или дефис (-). Используйте тот разделитель, который используется в проекте. Если единого подхода нет – используйте “_”.

Примеры подходящих имён:

  • my_useful_class.cc
  • my-useful-class.cc
  • myusefulclass.cc
  • myusefulclass_test.cc // _unittest and _regtest are deprecated.

C++ файлы должны заканчиваться на .cc, заголовочные – на .h. Файлы, включаемые как текст должны заканчиваться на .inc (см. также секцию Независимые заголовочные файлы).

Не используйте имена, уже существующие в /usr/include, такие как db.h.

Старайтесь давать файлам специфичные имена. Например, http_server_logs.h лучше чем logs.h. Когда файлы используются парами, лучше давать им одинаковые имена. Например, foo_bar.h и foo_bar.cc (и содержат класс FooBar).

Имена типов

Имена типов начинаются с прописной буквы, каждое новое слово также начинается с прописной буквы. Подчёркивания не используются: MyExcitingClass, MyExcitingEnum.

Имена всех типов – классов, структур, псевдонимов, перечислений, параметров шаблонов – именуются в одинаковом стиле. Имена типов начинаются с прописной буквы, каждое новое слово также начинается с прописной буквы. Подчёркивания не используются. Например:

// classes and structs
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...

// typedefs
typedef hash_map<UrlTableProperties *, std::string> PropertiesMap;

// using aliases
using PropertiesMap = hash_map<UrlTableProperties *, std::string>;

// enums
enum UrlTableErrors { ...

Имена переменных

Имена переменных (включая параметры функций) и членов данных пишутся строчными буквами с подчёркиванием между словами. Члены данных классов (не структур) дополняются подчёркиванием в конце имени. Например: a_local_variable, a_struct_data_member, a_class_data_member_.

Имена обычных переменных

Например:

std::string table_name;  // OK - строчные буквы с подчёркиванием
std::string tableName;   // Плохо - смешанный стиль

Члены данных класса

Члены данных классов, статические и нестатические, именуются как обычные переменные с добавлением подчёркивания в конце.

class TableInfo {
  ...
 private:
  std::string table_name_;  // OK - подчёркивание в конце
  static Pool<TableInfo>* pool_;  // OK.
};

Члены данных структуры

Члены данных структуры, статические и нестатические, именуются как обычные переменные. К ним не добавляется символ подчёркивания в конце.

struct UrlTableProperties {
  std::string name;
  int num_entries;
  static Pool<UrlTableProperties>* pool;
};

См. также Структуры vs Классы, где описано когда использовать структуры, когда классы.

Имена констант

Объекты объявляются как constexpr или const, чтобы значение не менялось в процессе выполнения. Имена констант начинаются с символа “k”, далее идёт имя в смешанном стиле (прописные и строчные буквы). Подчёркивание может быть использовано в редких случаях когда прописные буквы не могут использоваться для разделения. Например:

const int kDaysInAWeek = 7;
const int kAndroid8_0_0 = 24;  // Android 8.0.0

Все аналогичные константные объекты со статическим типом хранилища (т.е. статические или глобальные, подробнее тут: Storage Duration) именуются также. Это соглашение является необязательным для переменных в других типах хранилища (например, автоматические константные объекты).

Имена функций

Обычные функции именуются в смешанном стиле (прописные и строчные буквы); функции доступа к переменным (accessor и mutator) должны иметь стиль, похожий на целевую переменную.

Обычно имя функции начинается с прописной буквы и каждое слово в имени пишется с прописной буквы.

AddTableEntry()
DeleteUrl()
OpenFileOrDie()

(Аналогичные правила применяются для констант в области класса или пространства имён (namespace) которые представляют собой часть API и должны выглядеть как функции (и то, что они не функции – некритично))

Accessor-ы и mutator-ы (функции get и set) могут именоваться наподобие соответствующих переменных. Они часто соответствуют реальным переменным-членам, однако это не обязательно. Например, int count() и void set_count(int count).

Именование пространства имён (namespace)

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

Пространство имён верхнего уровня – это обычно название проекта или команды (которая делала код). Код должен располагаться в директории (или поддиректории) с именем, соответствующим пространству имён.

Не забывайте правило не использовать аббревиатуры – к пространствам имён это также применимо. Коду внутри вряд ли потребуется упоминание пространства имён, поэтому аббревиатуры – это лишнее.

Избегайте использовать для вложенных пространств имён известные названия. Коллизии между именами могут привести к сюрпризам при сборке. В частности, не создавайте вложенных пространств имён с именем std. Рекомендуются уникальные идентификаторы проекта (websearch::index, websearch::index_util) вместо небезопасных к коллизиям websearch::util.

Для internal / внутренних пространств имён коллизии могут возникать при добавлении другого кода (внутренние хелперы имеют свойство повторяться у разных команд). В этом случае хорошо помогает использование имени файла для именования пространства имён. (websearch::index::frobber_internal для использования в frobber.h)

Имена перечислений

Перечисления (как с ограничениями на область видимости (scoped), так и без (unscoped)) должны именоваться либо как константы, либо как macros. Т.е.: либо kEnumName, либо ENUM_NAME.

Предпочтительно именовать отдельные значения в перечислителе как константы. Однако, допустимо именовать как макросы. Имя самого перечисления UrlTableErrorsAlternateUrlTableErrors), это тип. Следовательно, используется смешанный стиль.

enum UrlTableErrors {
  kOk = 0,
  kErrorOutOfMemory,
  kErrorMalformedInput,
};
enum AlternateUrlTableErrors {
  OK = 0,
  OUT_OF_MEMORY = 1,
  MALFORMED_INPUT = 2,
};

Вплоть до января 2009 года стиль именования значений перечисления был как у макросов. Это создавало проблемы дублирования имён макросов и значений перечислений. Применение стиля констант решает проблему и в новом коде предпочтительно использовать стиль констант. Однако, старый код нет необходимости переписывать (пока нет проблем дублирования).

Имена макросов

Вы ведь не собираетесь определять макросы? На всякий случай (если собираетесь), они должны выглядеть так: MY_MACRO_THAT_SCARES_SMALL_CHILDREN_AND_ADULTS_ALIKE.

Пожалуйста прочтите как определять макросы; Обычно, макросы не должны использоваться. Однако, если они вам абсолютно необходимы, именуйте их прописными буквами с символами подчёркивания.

#define ROUND(x) ...
#define PI_ROUNDED 3.0

Исключения из правил именования

Если вам нужно именовать что-то, имеющее аналоги в существующем C или C++ коде, то следуйте используемому в коде стилю.

bigopen()
имя функции, образованное от open()
uint
похож на стандартный тип
bigpos
struct или class, образованный от pos
sparse_hash_map
STL-подобная сущность; следуйте стилю STL
LONGLONG_MAX
константа, такая же как INT_MAX

Комментарии

Комментарии являются обязательными для кода (если вы планируете его читать). Следующие правила описывают, что вы должны комментировать и как. Но помните: хотя комментарии очень важны, идеальный код сам себя документирует. Использование “говорящих” имён для типов и переменных намного лучше, чем непонятные имена, которые потом требуется расписывать в комментариях.

Комментируйте код с учётом его следующих читателей: программистов, которым потребуется разбираться в вашем коде. Учтите, что следующим читателем можете стать вы!

Стиль комментариев

Используйте либо // либо /* */, пока не нарушается единообразие.

Вы можете использовать либо // либо /* */, однако // намного предпочтительнее. Однако, всегда согласовывайте ваш стиль комментариев с уже существующим кодом.

Комментарии в шапке файла

В начало каждого файла вставляйте шапку с лицензией.

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

Правовая информация и список авторов

Каждый файл должен содержать информацию о лицензии. Формат описания зависит от лицензии, используемой в проекте. У каждой лицензии (Apache 2.0, BSD, LGPL, GPL, др.) могут быть свои требования к оформлению.

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

Содержимое файлов

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

Не дублируйте комментарии в .h и .cc файлах – со временем комментарии становятся разными.

Комментарии класса

Каждое объявление класса (кроме совсем очевидных) должно сопровождаться комментарием, для чего класс и как им пользоваться.

// Перебор содержимого GargantuanTable.
// Пример:
//    GargantuanTableIterator* iter = table->NewIterator();
//    for (iter->Seek("foo"); !iter->done(); iter->Next()) {
//      process(iter->key(), iter->value());
//    }
//    delete iter;
class GargantuanTableIterator {
  ...
};

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

В комментарии к классу также можно привести короткие примеры кода, показывающие как проще использовать класс.

Обычно класс объявляется/определяется в разных файлах (.h и .cc). Комментарии, описывающие использование класса должны быть рядом с определением интерфейса. Комментарии о тонкостях реализации должны быть рядом с кодом самих методов.

Комментарии функции

Комментарии к объявлению функции должны описывать использование функции (кроме самых очевидных случаев). Комментарии к определению функции описывают реализацию.

Объявление функции

Объявление каждой функции должно иметь комментарий (прямо перед объявлением), что функция делает и как ей пользоваться. Комментарий можно опустить, только если функция простая и использование очевидно (например, функции вычитывания значений переменных). Старайтесь начинать комментарии в изъявительном наклонении (“Открывает файл”). Использование повелительного наклонение (“Открыть файл”) – не рекомендуется. Комментарий описывает суть функции, а не то, как она это делает.

В комментарии к объявлению функции обратите внимание на следующее:

  • Что подаётся на вход функции, что возвращается в результате.
  • Для функции-члена класса: сохраняет ли экземпляр ссылки на аргументы, нужно ли освобождать память.
  • Выделяет ли функция память, которую должен удалить вызывающий код.
  • Могут ли быть аргументы nullptr.
  • Сложность (алгоритмическая) функции.
  • Допустим ли одновременный вызов из разных потоков. Что с синхронизацией?

Пример:

// Возвращает итератор по таблице. Клиент должен удалить 
// итератор после использования. Нельзя использовать итератор
// если соответствующий объект GargantuanTable был удалён.
//
// Итератор изначально указывает на начало таблицы.
//
// Этот метод эквивалентен следующему:
//    Iterator* iter = table->NewIterator();
//    iter->Seek("");
//    return iter;
// Если вы собираетесь сразу же делать новую операцию поиска,
// быстрее будет вызвать NewIterator() и избежать лишней операции поиска.
Iterator* GetIterator() const;

Однако не стоит разжёвывать очевидные вещи.

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

Комментируя конструкторы и деструкторы, учитывайте, что читатель кода знает их назначение. Поэтому комментарий типа “разрушает этот объект” – бестолковый. Можете описывать, что конструктор делает с аргументами (например, изменение владения на указатели) или какие именно операции по очистке делает деструктор. Если всё и так понятно – ничего не комментируйте. Вообще, обычно деструкторы не имеют комментариев (при объявлении).

Определение функций

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

Отметим что вы не должны повторять комментарий из объявления функции (из .h файла или т.п.). Можно кратко описать, что функция делает, однако основной упор должен быть как она это делает.

Комментарии к переменным

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

Член данных класса

Назначение каждого члена класса должно быть очевидно. Если есть неочевидные тонкости (специальные значения, завязки с другими членами, ограничения по времени жизни) – всё это нужно комментировать. Однако, если типа и имени достаточно – комментарии добавлять не нужно.

С другой стороны, полезными будут описания особых (и неочевидных) значений (nullptr или -1). Например:

private:
 // Используется для проверки выхода за границы
 // -1 - показывает, что мы не знаем сколько записей в таблице
 int num_total_entries_;

Глобальные переменные

Ко всем глобальным переменным следует писать комментарий о их назначении и (если не очевидно) почему они должны быть глобальными. Например:

// Общее количество тестов, прогоняемых в регрессионом тесте
const int kNumTestCases = 6;

Комментарии к реализации

Комментируйте реализацию функции или алгоритма в случае наличия неочевидных, интересных, важных кусков кода.

Описательные комментарии

Блоки кода, отличающиеся сложностью или нестандартностью, должны предваряться комментарием. Например:

// Делим результат на 2. Переменная x содержит флаг переноса
for (int i = 0; i < result->size(); ++i) {
  x = (x << 8) + (*result)[i];
  (*result)[i] = x >> 1;
  x &= 1;
}

Построчные комментарии

Строки кода с неочевидным смыслом желательно дополнять комментарием (обычно располагаемым в конце строки). Этот комментраий должен отделяться от кода 2-мя проблами. Например:

// Мапируем блок данных, если объём позволяет
mmap_budget = max<int64>(0, mmap_budget - index_->length());
if (mmap_budget >= data_size_ && !MmapData(mmap_chunk_bytes, mlock))
  return;  // Ошибку уже логировали

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

Комментарии к аргументам функций

Когда назначение аргумента функции неочевидно, подумайте о следующих вариантах:

  • Если аргумент есть фиксированное значение (literal constant) и это значение используется в разных блоках кода (и подразумевается, что это значение суть одно и тоже), вам следует создать константу и явно использовать её.
  • Возможно следует заменить аргумент типа bool на перечисление (enum). Это сделает аргумент само-определяющим.
  • Для функций, использующих несколько конфигурационных опций в аргументах, можно создать отдельный класс (или структуру), объединяющий все опции. И передавать в функцию экземпляр этого класса. Такой подход имеет несколько преимуществ: опции обозначаются именами, что объясняет их назначение. Уменьшается количество аргументов в функции – код легче писать и читать. И если вам понадобится добавить ещё опций, менять сам вызов функции не придётся.
  • Вместо сложных выражений в аргументах используйте промежуточную переменную, которой присвойте выражение.
  • В крайнем случае пишите комментарии в месте вызова для прояснения назначения аргументов.

Рассмотрим примеры:

// И какое назначение аргументов?
const DecimalNumber product = CalculateProduct(values, 7, false, nullptr);

Попробуем причесать код:

ProductOptions options;
options.set_precision_decimals(7);
options.set_use_cache(ProductOptions::kDontUseCache);
const DecimalNumber product =
    CalculateProduct(values, options, /*completion_callback=*/nullptr);

Что делать не нужно

Не объясняйте очевидное. В частности, не нужно объяснять вещи, очевидные для человека, знающего C++. Вместо этого, можно описать зачем этот код делает так (или вообще сделайте код само-описываемым).

Сравним:

// Ищём элемент в векторе.  <-- Плохо: очевидно же!
auto iter = std::find(v.begin(), v.end(), element);
if (iter != v.end()) {
  Process(element);
}

С этим:

// Обрабатывает (Process) "element" пока есть хоть один
auto iter = std::find(v.begin(), v.end(), element);
if (iter != v.end()) {
  Process(element);
}

Само-описывающий код вообще не нуждается в комментариях. Комментарий на код выше может быть вообще очевидным (и не нужным):

if (!IsAlreadyProcessed(element)) {
  Process(element);
}

Пунктуация, орфография и грамматика

Обращайте внимание на пунктуацию, орфографию и грамматику: намного проще читать грамотно написанные комментарии.

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

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

Комментарии TODO

Используйте комментарии TODO для временного кода или достаточно хорошего (промежуточного, не идеального) решения.

Комментарий должен включать строку TODO (все буквы прописные), за ней имя, адрес e-mail, ID дефекта или другая информация для идентификации разработчика и сущности проблемы, для которой написан TODO. Цель такого описания – возможность потом найти больше деталей. Наличие TODO с описанием не означает, что указанный программист исправит проблему. Поэтому, когда вы создаёте TODO, обычно там указано Ваше имя.

// TODO(kl@gmail.com): Используйте "*" для объединения.
// TODO(Zeke) Изменить для связывания.
// TODO(bug 12345): удалить фунционал "Последний посетитель".

Если ваш TODO вида “В будущем сделаем по-другому”, то указывайте либо конкретную дату (“Исправить в ноябре 2005”), либо событие (“Удалить тот код, когда все клиенты будут обрабатывать XML запросы”).

Форматирование

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

Для корректного форматирования мы создали файл настроек для emacs.

Длина строк

Желательно ограничивать длину строк кода 80-ю символами.

Это правило немного спорное, однако масса уже существющего кода придерживается этого принципа, и мы также поддерживаем его.

Приверженцы правила утверждают, что строки длиннее не нужны, а постоянно подгонять размеры окон утомительно. Кроме того, некоторые размещают окна с кодом рядом друг с другом и не могут произвольно увеличивать ширину окон. При этом ширина в 80 символов – исторический стандарт, зачем его менять?.

Другая сторона утверждает, что длинные строки могут улучшить читабельность кода. 80 символов – пережиток мейнфреймов 1960-х. Современные экраны вполне могут показывать более длинные строки.

80 символов – максимум.

Строка может превышать предел в 80 символов если:

  • комментарий при разделении потеряет в понятности или лёгкости копирования. Например, комментарий с примером команды или URL-ссылкой, длиннее 80 символов.
  • строковый литерал/имя, длиной более 80 символов. Исключением является тестовый код, который желательно размещать в начале файла.
  • выражения с include.
  • Блокировка от повторного включения
  • using декларации

Не-ASCII символы

Не-ASCII символы следует использоваться как можно реже, кодировка должна быть UTF-8.

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

Кодировка hex также допустима, особенно если она улучшает читабельность. Например, "\xEF\xBB\xBF" или u8"\uFEFF" – неразрывный пробел нулевой длины в Юникоде, и который не должен отображаться в правильном UTF-8 тексте.

Используйте префикс u8 чтобы литералы вида \uXXXX кодировались в UTF-8. Не используйте его для строк, содержащих не-ASCII символы уже закодированные в UTF-8 – можете получить корявый текст если компилятор не распознает исходный код как UTF-8.

Избегайте использования символов C++11 char16_t и char32_t т.к. они нужны для не-UTF-8 строк. По тем же причинам не используйте wchar_t (кроме случаев работы с Windows API, использующий wchar_t).

Пробелы против Табуляции

Используйте только пробелы для отступов. 2 пробела на один отступ.

Мы используем пробелы для отступов. Не используйте табуляцию в своём коде – настройте свой редактор на вставку пробелов при нажатии клавиши Tab.

Объявления и определения функций

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

Пример правильного оформления функции:

ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) {
  DoSomething();
  ...
}

В случае если одной строки мало:

ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2,
                                             Type par_name3) {
  DoSomething();
  ...
}

или, если первый параметр также не помещается:

ReturnType LongClassName::ReallyReallyReallyLongFunctionName(
    Type par_name1,  // Отступ 4 пробела
    Type par_name2,
    Type par_name3) {
  DoSomething();  // Отступ 2 пробела
  ...
}

Несколько замечаний:

  • Выбирайте хорошие имена для параметров.
  • Имя параметра можно опустить, если он не используется в определении функции.
  • Если тип возвращаемого значения и имя функции не помещаются в одной строке, тип оставьте на одной строке, имя функции перенесите на следующую. В этом случае не делайте дополнительный отступ перед именем функции.
  • Открывающая круглая скобка всегда находится на одной строке с именем функции.
  • Не вставляйте пробелы между именем функции и открывающей круглой скобкой.
  • Не вставляйте пробелы между круглыми скобками и параметрами.
  • Открывающая фигурная скобка всегда в конце последней строки определения. Не переносите её на новую строку.
  • Закрывающая фигурная скобка располагается либо на отдельной строке, либо на той же строке, где и открывающая скобка.
  • Между закрывающей круглой скобкой и открывающей фигурной скобкой должен быть пробел.
  • Старайтесь выравнивать все параметры.
  • Стандартный отступ – 2 пробела.
  • При переносе параметров на другую строку используйте отступ 4 пробела.

Можно опустить имя неиспользуемых параметров, если это очевидно из контекста:

class Foo {
 public:
  Foo(const Foo&) = delete;
  Foo& operator=(const Foo&) = delete;
};

Неиспользуемый параметры с неочевидным контекстом следует закомментировать в определении функции:

class Shape {
 public:
  virtual void Rotate(double radians) = 0;
};

class Circle : public Shape {
 public:
  void Rotate(double radians) override;
};

void Circle::Rotate(double /*radians*/) {}
// Плохой стиль - если кто-то потом захочет изменить реализацию функции,
// назначение параметра не ясно.
void Circle::Rotate(double) {}

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

ABSL_MUST_USE_RESULT bool IsOk();

Лямбды

Форматируйте параметры и тело выражения аналогично обычной функции, список захватываемых переменных – как обычный список.

Для захвата переменных по ссылке не ставьте пробел между амперсандом (&) и именем переменной.

int x = 0;
auto x_plus_n = [&x](int n) -> int { return x + n; }

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

std::set<int> blacklist = {7, 8, 9};
std::vector<int> digits = {3, 9, 1, 8, 4, 7, 1};
digits.erase(std::remove_if(digits.begin(), digits.end(), [&blacklist](int i) {
               return blacklist.find(i) != blacklist.end();
             }),
             digits.end());

Числа с плавающей запятой

Числа с плавающей запятой всегда должны быть с десятичной точкой и числами по обе стороны от неё (даже в случае экспоненциальной нотации). Такой подход улучшить читабельность: все числа с плавающей запятой будут в одинаковом формате, не спутаешь с целым числом, и символы E/e экспоненциальной нотации не примешь за шестнадцатеричные цифры. Помните, что число в экспоненциальной нотации не является целым числом.

float f = 1.f;
long double ld = -.5L;
double d = 1248e6;
float f = 1.0f;
float f2 = 1;   // Также правильно
long double ld = -0.5L;
double d = 1248.0e6;

Вызов функции

Следует либо писать весь вызов функции одной строкой, либо размещать аргументы на новой строке. И отступ может быть либо по первому аргументу, либо 4 пробела. Старайтесь минимизировать количество строк, размещайте по несколько аргументов на каждой строке.

Формат вызова функции:

bool result = DoSomething(argument1, argument2, argument3);

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

bool result = DoSomething(averyveryveryverylongargument1,
                          argument2, argument3);

Допускается размещать аргументы на нескольких строках с отступом в 4 пробела:

if (...) {
  ...
  ...
  if (...) {
    bool result = DoSomething(
        argument1, argument2,  // Отступ 4 пробела
        argument3, argument4);
    ...
  }

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

Если несколько аргементов в одной строке ухудшают читабельность (из-за сложности или запутанности выражений), попробуйте создать для аргументов “говорящие” переменные:

int my_heuristic = scores[x] * y + bases[x];
bool result = DoSomething(my_heuristic, x, y, z);

Или разместите сложный аргумент на отдельной строке и добавьте поясняющий комментарий:

bool result = DoSomething(scores[x] * y + bases[x],  // Небольшая эвристика
                          x, y, z);

Если в вызове функции ещё есть аргументы, которые желательно разместить на отдельной строке – размещайте. Решение должно основываться улучшении читабельность кода.

Иногда аргументы формируют структуру. В этом случае форматируйте аргументы согласно требуемой структуре:

// Преобразование с помощью матрицы 3x3
my_widget.Transform(x1, x2, x3,
                    y1, y2, y3,
                    z1, z2, z3);

Форматирование списка инициализации

Форматируйте список инициализации аналогично вызову функции.

Если список в скобках следует за именем (например, имя типа или переменной), форматируйте {} как будто это вызов функции с этим именем. Даже если имени нет, считайте что оно есть, только пустое.

// Пример списка инициализации на одной строке.
return {foo, bar};
functioncall({foo, bar});
std::pair<int, int> p{foo, bar};

// Когда хочется разделить на строки.
SomeFunction(
    {"assume a zero-length name before {"},
    some_other_function_parameter);
SomeType variable{
    some, other, values,
    {"assume a zero-length name before {"},
    SomeOtherType{
        "Very long string requiring the surrounding breaks.",
        some, other values},
    SomeOtherType{"Slightly shorter string",
                  some, other, values}};
SomeType variable{
    "This is too long to fit all in one line"};
MyType m = {  // Here, you could also break before {.
    superlongvariablename1,
    superlongvariablename2,
    {short, interior, list},
    {interiorwrappinglist,
     interiorwrappinglist2}};

Условия

Старайтесь не вставлять пробелы с внутренней стороны скобок. Размещайте if и else на разных строках.

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

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

if (condition) {  // без пробелов внутри скобок
  ...  // отступ 2 пробела
} else if (...) {  // 'else' находится на строке с закрывающей скобкой
  ...
} else {
  ...
}

Если используется формат с пробелами:

if ( condition ) {  // пробелы внутри скобок
  ...  // отступ 2 пробела
} else {  // 'else' находится на строке с закрывающей скобкой
  ...
}

Заметьте, что в любом случае должен быть пробел между if и открывающей скобкой. Также нужен пробел между закрывающей скобкой и фигурной скобкой (если она есть).

if(condition) {   // Плохо - нет пробела после 'if'
if (condition){   // Плохо - нет пробела перед {
if(condition){    // Дважды плохо
if (condition) {  // Хороший код - правильное количество пробелов после 'if' и перед {

Короткие условия можно записать в одну строку, если это улучшит читабельность. Используйте этот вариант только если строка короткая и условие не содержит секцию else.

if (x == kFoo) return new Foo();
if (x == kBar) return new Bar();

Не используйте сокращённый вариант, если есть секция else:

// Плохо - условие в одну строку, хотя есть 'else'
if (x) DoThis();
else DoThat();

Обычно фигурные скобки не требуются для короткого условия, однако они допустимы. Также сложные условия или код лучше читаются при наличии фигурных скобок. Часто требуют, чтобы любой if был со скобками.

if (condition)
  DoSomething();  // отступ 2 пробела

if (condition) {
  DoSomething();  // отступ 2 пробела
}

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

// Плохо - фигурные скобки у 'if', у 'else' - нет
if (condition) {
  foo;
} else
  bar;

// Плохо - фигурные скобки у 'else', у 'if' - нет
if (condition)
  foo;
else {
  bar;
}
// Хорошо - фигурные скобки и у 'if' и у 'else'
if (condition) {
  foo;
} else {
  bar;
}

Циклы и switch-и

Конструкция switch может использовать скобки для блоков. Описывайте нетривиальные переходы между вариантами. Скобки необязательны для циклов с одним выражением. Пустой цикл должен использовать либо пустое тело в скобках или continue.

Блоки case в switch могут как быть с фигурными скобками, так быть и без них (на ваш выбор). Если же скобки используются, используйте формат, описанный ниже.

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

switch (var) {
  case 0: {  // Отступ 2 пробела
    ...      // Отступ 4 пробела
    break;
  }
  case 1: {
    ...
    break;
  }
  default: {
    assert(false);
  }
}

Переход с одной метки на следующую должен быть помечен макросом ABSL_FALLTHROUGH_INTENDED; (определён в absl/base/macros.h). Размещайте ABSL_FALLTHROUGH_INTENDED; в точке, где будет переход. Исключение из этого правила – последовательные метки без кода, в этом случае помечать ничего не нужно.

switch (x) {
  case 41:  // Без пометок
  case 43:
    if (dont_be_picky) {
      // Используйте макрос вместо (или совместно) с комментарием о переходе
      ABSL_FALLTHROUGH_INTENDED;
    } else {
      CloseButNoCigar();
      break;
    }
  case 42:
    DoSomethingSpecial();
    ABSL_FALLTHROUGH_INTENDED;
  default:
    DoSomethingGeneric();
    break;
}

Скобки являются опциональными для циклов с одной операцией.

for (int i = 0; i < kSomeNumber; ++i)
  printf("I love you\n");

for (int i = 0; i < kSomeNumber; ++i) {
  printf("I take it back\n");
}

Пустой цикл должен быть оформлен либо как пара скобок, либо как continue без скобок. Не используйте одиночную точку с запятой.

while (condition) {
  // Повторять до получения false
}
for (int i = 0; i < kSomeNumber; ++i) {}  // Хорошо. Если разбить на две строки - тоже будет хорошо
while (condition) continue;  // Хорошо - continue указывает на отсутствие дополнительной логики
while (condition);  // Плохо - выглядит как часть цикла do/while

Указатели и ссылки

Вокруг ‘.’ и ‘->’ не ставьте пробелы. Оператор разыменования или взятия адреса должен быть без пробелов.

Ниже приведены примеры правильного форматирования выражений с указателями и ссылками:

x = *p;
p = &x;
x = r.y;
x = r->y;

Отметим:

  • ‘.’ и ‘->’ используются без пробелов.
  • Операторы * или & не отделяются пробелами.

При объявлении переменной или аргумента можно размещать ‘*’ как к типу, так и к имени:

// Отлично, пробел до *, &
char *c;
const std::string &str;

// Отлично, пробел после *, &
char* c;
const std::string& str;

Старайтесь использовать единый стиль в файле кода, при модификации существующего файла применяйте используемое форматирование.

Допускается объявлять несколько переменных одним выражением. Однако не используйте множественное объявление с указателями или ссылками – это может быть неправильно понято.

// Хорошо - читабельно
int x, y;
int x, *y;  // Плохо - не используйте множественное объявление с & или *
char * c;  // Плохо - пробелы с обеих сторон *
const std::string & str;  // Плохо - пробелы с обеих сторон &

Логические выражения

Если логическое выражение очень длинное (превышает типовое значение), используйте единый подход к разбивке выражения на строки.

Например, здесь при переносе оператор AND располагается в конце строки:

if (this_one_thing > this_other_thing &&
    a_third_thing == a_fourth_thing &&
    yet_another && last_one) {
  ...
}

Отметим, что разбиение кода (согласно примеру) производится так, чтобы && и оператор AND завершали строку. Такой стиль чаще используется с коде Google, хотя расположение операторов в начале строки тоже допустимо. Также, можете добавлять дополнительные скобки для улучшения читабельности. Учтите, что использование операторов в виде пунктуации (такие как && и ~) более предпочтительно, что использование операторов в виде слов and и compl.

Возвращаемые значения

Не заключайте простые выражения return в скобки.

Используйте скобки в return expr; только если бы вы использовали их в выражении вида x = expr;.

return result;                  // Простое выражение - нет скобок
// Скобки - Ок. Они улучшают читабельность выражения
return (some_long_condition &&
        another_condition);
return (value);                // Плохо. Например, вы бы не стали писать var = (value);
return(result);                // Плохо. return - это не функция!

Инициализация переменных и массивов

Что использовать: =, () или {} – это ваш выбор.

Вы можете выбирать между вариантами =, () и {}. Следующие примеры кода корректны:

int x = 3;
int x(3);
int x{3};
std::string name = "Some Name";
std::string name("Some Name");
std::string name{"Some Name"};

Будьте внимательны при использовании списка инициализации {...} для типа, у которого есть конструктор с std::initializer_list. Компилятор предпочтёт использовать конструктор std::initializer_list при наличии списка в фигурных скобках. Заметьте, что пустые фигурные скобки {} – это особый случай и будет вызван конструктор по-умолчанию (если он доступен). Для явного использования конструктора без std::initializer_list применяйте круглые скобки вместо фигурных.

std::vector<int> v(100, 1);  // Вектор из сотни единиц
std::vector<int> v{100, 1};  // Вектор из 2-х элементов: 100 и 1

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

int pi(3.14);  // Ок: pi == 3
int pi{3.14};  // Ошибка компиляции: "сужающее" преобразование

Директивы препроцессора

Знак # (признак директивы препроцессора) должен быть в начале строки.

Даже если директива препроцессора относится к вложенному коду, директивы пишутся с начала строки.

// Хорошо - директивы с начала строки
  if (lopsided_score) {
#if DISASTER_PENDING      // Корректно - начинается с начала строки
    DropEverything();
# if NOTIFY               // Пробелы после # - ок, но не обязательно
    NotifyClient();
# endif
#endif
    BackToNormal();
  }
// Плохо - директивы с отступами
  if (lopsided_score) {
    #if DISASTER_PENDING  // Неправильно! "#if" должна быть в начале строки
    DropEverything();
    #endif                // Неправильно! Не делайте отступ для "#endif"
    BackToNormal();
  }

Форматирование классов

Размещайте секции в следующем порядке: public, protected и private. Отступ – один пробел.

Ниже описан базовый формат для класса (за исключением комментариев, см. описание Комментирование класса):

class MyClass : public OtherClass {
 public:      // Отступ 1 пробел
  MyClass();  // Обычный 2-х пробельный отступ
  explicit MyClass(int var);
  ~MyClass() {}

  void SomeFunction();
  void SomeFunctionThatDoesNothing() {
  }

  void set_some_var(int var) { some_var_ = var; }
  int some_var() const { return some_var_; }

 private:
  bool SomeInternalFunction();

  int some_var_;
  int some_other_var_;
};

Замечания:

  • Имя базового класса пишется в той же строке, что и имя наследуемого класса (конечно, с учётом ограничения в 80 символов).
  • Ключевые слова public:, protected:, и private: должны быть с отступом в 1 пробел.
  • Перед каждым из этих ключевых слов должна быть пустая строка (за исключением первого упоминания). Также в маленьких классах пустые строки можно опустить.
  • Не добавляйте пустую строку после этих ключевых слов.
  • Секция public должна быть первой, за ней protected и в конце секция private.
  • См. Порядок объявления для выстраивания деклараций в каждой из этих секций.

Списки инициализации конструктора

Списки инициализации конструктора могут быть как в одну строку, так и на нескольких строках с 4-х пробельным отступом.

Ниже представлены правильные форматы для списков инициализации:

// Всё в одну строку
MyClass::MyClass(int var) : some_var_(var) {
  DoSomething();
}

// Если сигнатура и список инициализации не помещается на одной строке,
// нужно перенести двоеточие и всё что после него на новую строку
MyClass::MyClass(int var)
    : some_var_(var), some_other_var_(var + 1) {
  DoSomething();
}

// Если список занимает несколько строк, то размещайте каждый элемент на
// отдельной строке и всё выравниваем
MyClass::MyClass(int var)
    : some_var_(var),             // Отступ 4 пробела
      some_other_var_(var + 1) {  // Выравнивание по предыдущему
  DoSomething();
}

// Как и в других случаях, фигурные скобки могут размещаться на одной строке
MyClass::MyClass(int var)
    : some_var_(var) {}

Форматирование пространств имён

Содержимое в пространстве имён пишется без отступа.

Пространство имён не добавляет отступов. Например:

namespace {

void foo() {  // Хорошо. Без дополнительного отступа
  ...
}

}  // namespace

Не делайте отступов в пространстве имён:

namespace {

  // Плохо. Сделан отступ там, где не нужно
  void foo() {
    ...
  }

}  // namespace

При объявлении вложенных пространств имён, размещайте каждое объявление на отдельной строке.

namespace foo {
namespace bar {

Горизонтальная разбивка

Используйте горизонтальную разбивку в зависимости от ситуации. Никогда не добавляйте пробелы в конец строки.

Общие принципы

void f(bool b) {  // Перед открывающей фигурной скобкой всегда ставьте пробел
  ...
int i = 0;  // Обычно перед точкой с запятой нет пробела
// Пробелы внутри фигурных скобок для списка инициализации можно добавлять на ваш выбор.
// Если вы добавляете пробелы, то ставьте их с обеих сторон
int x[] = { 0 };
int x[] = {0};

// Пробелы вокруг двоеточия в списках наследования и инициализации
class Foo : public Bar {
 public:
  // Для inline-функции добавляйте 
  // пробелы внутри фигурных скобок (кроме пустого блока)
  Foo(int b) : Bar(), baz_(b) {}  // Пустой блок без пробелов
  void Reset() { baz_ = 0; }  // Пробелы разделяют фигурные скобки и реализацию
  ...

Добавление разделительных пробелов может мешать при слиянии кода. Поэтому: Не добавляйте разделительных пробелов в существующий код. Вы можете удалить пробелы, если уже модифицировали эту строку. Или сделайте это отдельной операцией (предпочтительно, чтобы с этим кодом при этом никто не работал).

Циклы и условия

if (b) {          // Пробел после ключевого слова в условии или цикле
} else {          // Пробелы вокруг else
}
while (test) {}   // Внутри круглых скобок обычно не ставят пробел
switch (i) {
for (int i = 0; i < 5; ++i) {
// Циклы и условия могут могут внутри быть с пробелам. Но это редкость.
// В любом случае, будьте последовательны
switch ( i ) {
if ( test ) {
for ( int i = 0; i < 5; ++i ) {
// В циклах после точки с запятой всегда ставьте пробел
// Также некоторые любят ставить пробел и перед точкой с запятой, но это редкость
for ( ; i < 5 ; ++i) {
  ...

// В циклы по диапазону всегда ставьте пробел до двоеточия и после
for (auto x : counts) {
  ...
}
switch (i) {
  case 1:         // Перед двоеточнием в case нет пробела
    ...
  case 2: break;  // После двоеточия есть пробел, если дальше (на той же строке) идёт код

Операторы

// Операторы присваивания всегда окружайте пробелами
x = 0;

// Другие бинарные операторы обычно окружаются пробелами,
// хотя допустимо умножение/деление записывать без пробелов.
// Между выражением внутри скобок и самими скобками не вставляйте пробелы
v = w * x + y / z;
v = w*x + y/z;
v = w * (x + z);

// Унарные операторы не отделяйте от их аргумента
x = -5;
++x;
if (x && !y)
  ...

Шаблоны и приведение типов

// Не ставьте пробелы внутри угловых скобок (< и >),
// перед <, между >( в приведении
std::vector<std::string> x;
y = static_cast<char*>(x);

// Пробелы между типом и знаком указателя вполне допустимы. Но смотрите на уже используемый формат кода
std::vector<char *> x;

Вертикальная разбивка

Сведите к минимуму вертикальное разбиение.

Это больше принцип, нежели правило: не добавляйте пустых строк без особой надобности. В частности, ставьте не больше 1-2 пустых строк между функциями, не начинайте функцию с пустой строки, не заканчивайте функцию пустой строкой, и старайтесь поменьше использовать пустые строки. Пустая строка в блоке кода должна работать как параграф в романе: визуально разделять две идеи.

Базовый принцип: чем больше кода поместится на одном экране, тем легче его понять и отследить последовательность выполнения. Используйте пустую строку исключительно с целью визуально разделить эту последовательность.

Несколько полезных замечаний о пустых строках:

  • Пустая строка в начале или в конце функции не улучшит читабельность.
  • Пустые строки в цепочке блоков if-else могут улучшить читабельность.
  • Пустая строка перед строкой с комментарием обычно помогает читабельности кода – новый комментарий обычно предполагает завершение старой мысли и начало новой идеи. И пустая строка явно на это намекает.

Исключения из правил

Соглашения по кодированию, описанные выше являются обязательными. Однако, как и в любых правилах, иногда в них есть исключения, которые сейчас и обсудим.

Существующий код, не соответствующий стилю

Допустимо отклоняться от правил, если производится работа с кодом, не соответствующим этому руководству.

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

Программирование под Windows

Программисты под Windows могут использовать особенный набор соглашений о кодировании, основанный на стиле заголовочных файлов в Windows и другом коде от Microsoft. Так как хочется сделать, чтобы код был понятным для всех, то рекомендуется использовать единое руководство по стилю в C++, одинаковое для всех платформ.

Повторим несколько рекомендаций, которые отличают данное руководство от стиля Windows:

  • Не используйте венгерскую нотацию (например, именование целочисленной переменной как iNum). Вместо этого используйте соглашения об именовании от Google, включая расширение .cc для файлов с исходным кодом.
  • Windows определяет собственные синонимы для базовых типов, такие как DWORD, HANDLE и др. Понятно, что при вызове Windows API рекомендуется использовать именно их. И всё равно, старайтесь определять типы, максимально похожие на C++. Например, используйте const TCHAR * вместо LPCTSTR.
  • При компиляции кода с помощью Microsoft Visual C++ установите уровень предупреждений 3 или выше. Также установите настройку, чтобы трактовать все предупреждения как ошибки.
  • Не используйте #pragma once. Вместо этого используйте стандартную защиту от повторного включения, описанную в руководстве от Google. Компоненты пути в имени для макроопределения защиты должны быть относительными к корню проекта.
  • Вообще, не используйте нестандартные расширения, такие как #pragma и __declspec (исключение для случаев крайней необходимости). Использование __declspec(dllimport) и __declspec(dllexport) допустимо, однако следует оформить их как макросы DLLIMPORT и DLLEXPORT: в этом случае их можно легко заблокировать, если код будет распространяться.

С другой стороны, есть правила, которые можно нарушать при программировании под Windows:

  • Обычно рекомендуется не использовать множественное наследование реализации; однако это требуется при использовании COM и некоторых классов ATL/WTL. В этом случае (при реализации COM или ATL/WTL) нарушение правила допустимо.
  • Хотя использование исключений в собственном коде не рекомендуется, они интенсивно используются в ATL и некоторых STL (в том числе и в варианте библиотеки от Visual C++). Когда используете ATL, следует определить _ATL_NO_EXCEPTIONS, чтобы запретить исключения. Желательно разобраться, можно ли запретить исключения и в STL, однако, если это не реализуемо, то допустимо разрешить исключения в компиляторе. (Учтите, что это разрешено только для компиляции STL. Пользовательский код всё равно не должен содержать обработчики исключений).
  • Типичный способ работы с прекомпилированными заголовочными файлами – включить такой файл первым в каждый файл исходников, обычно с именем StdAfx.h или precompile.h. Чтобы не создавать проблем при распространении кода, лучше избегать явного включения такого файла (за исключением precompile.cc). Используйте опцию компилятора /FI для автоматического включения такого файла.
  • Заголовочный файлы ресурсов (обычно resource.h), содержащий только макросы, может не следовать рекомендациям этого руководства.

Заключение

Руководствуйтесь здравым смыслом и придерживайтесь ПОСТОЯНСТВА в стиле.

Если редактируете чужой код, то потратьте несколько минут и ознакомьтесь со стилем этого кода. Если в нём используются пробелы вокруг if – следует делать также. Если комментарий отчёркнут линией из звёздочек, то в свой комментарии также следует добавить такую линию.

Основное предназначение руководства по стилю – это единый базис, это общий словарь, позволяющий не отвлекаться на вопросы “… как это написать …” и сосредоточиться на том, что написать. Здесь были описаны общие правила стиля, однако не стоит забывать и о текущем используемом стиле. Если в существующем коде делаются исправления, которые сильно отличаются от исходного кода, это усложняет чтение кода, сбивает с ритма. Постарайтесь этого избежать.

Так, хватит писать о коде. Лучше пишите сам код – это намного интереснее!

Перевод

Кислов Евгений, 2019-2021
email: dev@evgenykislov.com
evgenykislov.com

Актуальная версия перевода:
Руководство Google по стилю в C++