Project

General

Profile

Actions

Стиль кодирования на C++

Данный стиль кодирования основан на
Google C++ Style Guide(attachment:cppguide.pdf). В отличие от Google
мы не испытываем страха перед исключениями, помимо этого, у нас свои соглашения
об именовании переменных, констант и функций. На ~80%, текст соответствует
переводу с хабра.

Введение

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

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

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

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

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

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

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

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

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

Версия C++

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

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

В общем случае, каждый .cpp-файл должен иметь парный .hpp файл. Есть
исключения, например, модульные тесты или небольшие файлы, содержащие только
функцию main().

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

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

Самодостаточные заголовочные файлы

Заголовочные файлы должны быть самодостаточными и иметь расширение .hpp.
Незаголовочные файлы, предназначенные для включения в код, должны быть с
расширением .inc и использоваться осторожно.

Самодостаточность (self-containment) заголовочного файла означает, что
пользователи или утилиты для рефакторинга должны включать заголовочный файл без
каких-либо дополнительных условий. Например, файл a.hpp не должен требовать,
чтобы в файл с исходным кодом также был включен файл b.cpp.
Заголовочный файл должен иметь защиту от повторного включения и включать все
необходимые файлы.

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

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

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

Предпочтительнее использовать директиву #pragma once, чем пару
#ifndef/#define/#endif. Это связано с тем, что последний нарушает принцип DRY,
поскольку название заголовочного файла фигурирует в нескольких местах и при
рефакторинге приходится менять и сами макросы.

В некоторых случаях, например, если возможно включение одного и того же файла
из разных мест, #pragma once может не работать. В подобных ситуациях или
где-то еще, можно использовать макросы. В этом случае формат макроопределения
иметь следующий вид:<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-выражение может быть убрано. Таким образом, файл foo.cpp
должен напрямую включать bar.hpp, при использовании сущностей, объявленных
там, даже если файл foo.hpp также его включает. Это один из аспектов
самодостаточности.

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

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

Предварительное объявление (forward declaration) - это объявление сущности без
определения, например:

// In a C++ source file:
class B;
void FuncInB();
extern int variable_in_b;
ABSL_DECLARE_FLAG(flag_in_b);
  • Предварительное объявление может уменьшить время компиляции. Использование #include требует от компилятора сразу открывать и обрабатывать больше файлов.
  • Предварительное объявление позволит избежать ненужной перекомпиляции. Применение #include может привести к частой перекомпиляции из-за различных изменений в заголовочных файлах.

  • Предварительное объявление может скрывать зависимости от перекомпиляции при изменении заголовочных файлов.
  • Предварительное объявление хуже обрабатывается специальными утилитами.
  • При изменении API, предварительное объявление может стать некорректным. Как результат, предварительное объявление функции или шаблонов может блокировать изменение API: замена типов параметров на похожий, добавление параметров по умолчанию в шаблон, перенос в новое пространство имён.
  • Предварительное объявление символов из std:: может вызвать неопределённое поведение.
  • Иногда тяжело понять, что лучше подходит: предварительное объявление или обычный #include. Однако, замена #include на предварительное объявление может (без предупреждений) поменять смысл кода:
// b.hpp:
struct B {};
struct D : B {};
// good_user.cpp:
#include "b.hpp"

void f(B*);
void f(void*);
void test(D* x) { f(x); } // calls f(B*)

Если в коде заменить #include на предварительное объявление для структур
B и D, то test() будет вызывать f(void*).

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

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

Inline-функции

Inline-функции (встраиваемые функции) - это функции, тело которой
подставляется непосредственно в код при каждом вызове.

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

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

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

Вывод:

Хорошее правило: не объявлять функции встраиваемыми, если они длиннее 10
строк. Осторожно с деструкторами! Деструктор неявно вызывает деструкторы
родительских классов.
Еще одно правило: нет смысла делать функции встраиваемыми, если в них есть
switch или цикл.

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

Названия и порядок включения заголовочных файлов

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

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

#include "base/logging.hpp"

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

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

Отделяйте каждую непустую группу файлов пустой строкой.

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

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

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

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

#include "foo/server/fooserver.hpp"

#include <sys/types.h>
#include <unistd.h>

#include <string>
#include <vector>

#include "base/basictypes.hpp"
#include "base/commandlineflags.hpp"
#include "foo/server/bar.hpp"

Исключения

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

#include "foo/public/fooserver.h"

#include "base/port.h"

// For LANG_CXX11.
#ifdef LANG_CXX11
#include <initializer_list>
#endif // LANG_CXX11

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

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

Код, за некоторым исключением должен быть размещен в пространстве имен.
Имена пространств имен должны быть уникальными и основываться на названии и,
возможно, пути проекта. Не используйте using-директивы
(using namespace std). Не используйте inline пространства имен. Для
безымянных пространств имен см.
внутреннее связывание.

Пространства имен (namespaces) разбивают глобальную область видимости на
несколько, отдельных и именованных. Это помогает предотвращать коллизии имен в
глобальной области видимости.

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

Например, представим себе, что два различных проекта имеют класс 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-ов и предварительных объявлений классов из других пространств имён.
// В .hpp файле
namespace mynamespace {

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

// Определение функций внутри блока пространства имён.
void MyClass::Foo() {
...
}
} // namespace mynamespace
  • Не объявляйте ничего в пространстве имен std. В том числе и предварительные объявления.
  • Не используйте директиву using namespace ...; - это загрязняет глобальное пространство имен.
  • Не используйте using-объявление using foo::Bar; в заголовочных файлах.
  • Не используйте псевдонимы пространств имен в заголовочных файлах за исключением явно внутренних пространств имен.
// Укороченная запись для доступа к часто 
// используемым именам в .cpp файлах - Нормально
namespace baz = ::foo::bar::baz;
// Укороченная запись для доступа к часто используемым именам (в .hpp файле).
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-пространства имен.

Внутреннее связывание (Internal Linkage)

В том случае, если возможность ссылки на определения из .cpp-файла из других
файлов должна быть запрещена, необходимо использовать внутреннее связывание.
Для этого нужно либо объявить эти сущности как static, либо разместить их в
безымянном пространстве имен (unnamed namespace). Не используйте внутреннее
связывание в заголовочных файлах.

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

namespace {
...
} // namespace

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

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

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

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

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

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

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

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

Язык 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);
}

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

В статической области видимости (static storage duration) запрещены объекты,
за исключением тривиально удаляемых (trivially destructible). Это значит, что
деструктор такого объекта не должен делать ничего. Это же относится и к
деструкторам базовых классов, поскольку они будут вызваны неявно.
Более формально: тип не содержит пользовательского или виртуального деструктора,
а также все его члены и базовые типы являются тривиально-удаляемыми. Статические
переменные в функциях могут использовать динамическую инициализацию.
Динамическая инициализация в статических переменных классов или глобальных
переменных не приветствуется, но разрешена в некоторых случаях. Подробнее ниже.

Хорошее правило: глобальная переменная удовлетворяет данным требованиям,
если она может быть объявлена как constexpr.

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

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

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

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

Ограничения по уничтожению

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

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)

Порядок инициализации для всех выражений, кроме первого, не определен.

Необходимо использовать концепцию, которая в стандарте C++ называется
константной инициализацией (constant initialization). Это значит, что
выражение инициализации является константным выражением (constant expression),
и если объект инициализируется вызовом конструктора, то конструктор должен быть
объявлен как constexpr:

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

int n = 5;  // Отлично, 5 - константное выражение

Foo x(2);   // Отлично, 2 - константное выражение 
            // и вызывается constexpr конструктор

Foo a[] = { Foo(1), Foo(2), Foo(3) }; // Отлично

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

Обратите внимание на следующие примеры:

// Объявления, используемые ниже
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 в своей инициализации

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

Рекомендуемые практики

  • Глобальные строки: Если требуется именованная глобальная или статическая строковая переменная, используйте constexpr string_view, массив символов или указатель, указывающий на строковый литерал.
  • Динамические контейнеры (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-переменные

thread_local-переменные, которые объявлены вне функций должны
инициализироваться константами времени компиляции(true compile-time).
Используйте атрибут 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-переменные могут менее эффективны, чем встроенные (compiler intrinsic) функции компилятора.

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

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

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

ABSL_CONST_INIT thread_local Foo foo = ...;

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

Классы

Класс - это фундаментальная единица кода на C++. Обычно они используются очень
широко. Данный раздел описывает все рекомендации, которым необходимо следовать
при разработке классов.

Выполнение задач в конструкторах

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

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

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

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

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

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

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

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

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

Ключевое слово explicit может быть применено к конструктору или оператору
приведения типов, чтобы гарантировать, что неявное преобразование типов не
будет работать. Это применимо не только к неявному преобразованию, но и к
спискам инициализации. Например:

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.
Конструкторы, которые используют один параметр std::initializer_list
не должны использовать explicit для поддержки инициализации присваиванием
MyType m = {1, 2};.

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

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

Перемещаемый (movable) тип может быть инициализирован временным объектом или
ему может быть присвоен временный объект.

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

Для пользовательских типов данных поведение при копировании определяется в
конструкторе копирования (copy constructor) и в
операторе присваивания с копированием (copy-assignment operator).
Поведение при перемещении определяется в
конструкторе перемещения (move constructor) и в
операторе присваивания с перемещением (move-assignment operator), если они
присутствуют, либо поведением при копировании в противном случае.

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

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

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

Операции перемещения позволяют неявно и эффективно управлять передачей ресурсов
через rvalue-объекты. Это позволяет делать код проще.

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

Конструкторы копирования вызываются неявно, поэтому их выполнение легко упустить
из виду. Это может привести, например, к снижению производительности из-за
лишнего копирования данных.

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

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

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 например, структура или класс-интерфейс, то копируемость и перемещаемость определяется копируемостью и перемещаемостью всех открытых членов.
  • Если базовый класс явно некопируемый и неперемещаемый, наследные классы будут такими же. Однако, если базовый класс не объявляет это операции, то этого будет недостаточно для прояснения свойств наследуемых классов.
  • На заметку: Если конструктор копирования или оператор присваивания объявлен/удалён, то нужно и явно объявить/удалить оператор копирования, поскольку его статус неочевиден. Аналогично и для операций перемещения.

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

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

Структуры против классов

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

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

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

Если сомневаетесь, используйте class.

Для единообразия с STL для stateless-типов можно использовать struct.
Например, type_traits и некоторые функторы.

Не забывайте о том, что у структур и классов разные
правила об именовании.

Структуры против пар и кортежей

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

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

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

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

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

Когда подкласс наследуется от базового класса, он включает все определения всех
данных и операций, определенных в базовом классе.
Наследованием интерфейса (Interface Inheritance) будем называть наследование
от чистого абстрактного базового класса (pure abstract base class) (без
определенных функций и состояния). Любое другое наследование будем называть
Наследованием реализации (implementation inheritance).

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

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

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

Любое наследование должно быть открытым (public). Если у вас возникает
потребность в закрытом наследовании, то необходимо создать закрытый член класса
который вы хотите сделать базовым. Можно использовать ключевое слово final на
классах, наследование от которых должно быть запрещено.

Не злоупотребляйте наследованием реализации. Композиция часто является более
предпочтительной. Используйте семантику является. Класс Bar стоит
наследовать от Foo, если можно дать утвердительный ответ на вопрос Bar
является Foo?

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

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

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

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

С++ позволяет программисту определять своим версии встроенных операторов,
используя для этого ключевое слово operator и пользовательский тип,
как один из параметров. Также ключевое слово operator позволяет создавать
пользовательские литералы, используя operator"" и операторы приведения типов,
например operator bool(). Это называется перегрузкой операторов.

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

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

  • Предоставление корректного, согласованного и логичного набора операторов для пользовательского типа требует усилий и может обернуться запутыванием и багами.
  • Злоупотребление перегрузкой операторов может привести к непонятному коду, особенно, если эти семантика данных операторов не соответствует общепринятой.
    • Все проблемы, связанные с перегрузкой функций распространяются и на перегрузку операторов.
  • Перегрузка операторов может быть не интуитивной в плане производительности, поскольку встроенные операции очень производительны.
  • Поиск в коде вызовов перегруженных операций требует особых утилит поиска, понимающих синтаксис C++.
  • Если запутаться с типами операндов перегружаемых операций, можно получить другой перегруженный оператор, а не ошибку компиляции, например foo < bar и &foo < &bar.
  • Перегрузка некоторых операторов является раскованной сама по себе. Например, перегрузка унарного &. Перегрузка операторов &&, ||, , не порядку исполнения встроенных операций.
  • Операторы могут определяться вне класса, поэтому есть риск того, что разные файлы могут содержать разное определение одного и того же оператора. Это может привести к трудноуловимым ошибкам времени исполнения.
  • Пользовательские литералы создают новые синтаксические формы, которые будут незнакомы даже продвинутым программистам. Например: "Hello World"sv как сокращение для std::string_view(«Hello World»).
  • Т. к. для пользовательских литералов не указывается пространство имен, придется использовать либо using-директиву, которая запрещена, либо using-объявление, запрещенное в заголовочных файлах.

Выводы:

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

Перегружайте операторы только для своих типов данных. Определяйте их в той же
паре .hpp/.cpp - файлов, что и тип данных, к которому они относятся. Таким
образом операции будут доступны тогда же, когда и исходный тип данных, а риск
нескольких определений операторов будет минимизирован. По возможности избегайте
определения шаблонных (template) операторов, поскольку они должны
соответствовать определенным правилам для всех аргументов. При перегрузке
операторов, перегружайте все связанные операторы, например, при перегрузке
оператора <, необходимо перегрузить все операторы сравнения. Убедитесь, что
они работают согласовано. Например, что операторы < и >
никогда не возвращают true одновременно.

По возможности, не делайте перегруженные операторы функциями-членами классов.
Если бинарный операнд определен как член класса, риски неявного преобразования
существуют только для правого оператора, а если как глобальная функция - для
обоих. Будет странно, если a < b компилируется, а b > a - нет.

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

Не перегружайте операторы &&, ||, ,(запятая) и унарный &.
Не используйте пользовательские литералы.

Операторы преобразования типов обсуждаются и в секции про
неявное преобразование типов.
Оператор = затрагивается в секции про
конструкторы копирования.
Перегрузка оператора << затрагивается в секции про
потоки. Также обратите внимание не секцию про
перегрузку функций, которая затрагивает перегрузку в
целом.

Контроль доступа

Члены-данные класса должны быть закрытыми (private), если это не константы
(const). Это упрощает работу с инвариантами ценой создания простых
клишированных функций (get/set-ассессоров), если не обходимо.

По техническим причинам члены данных фикстур для Google Test могут быть
protected.

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

Группируйте похожие объявления.

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

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

  1. Типы данных и псевдонимы (typedef, using, enum, вложенные структуры и классы, friend-типы данных).
  2. Статические константны.
  3. Фабричные методы
  4. Конструкторы и операторы присваивания
  5. Деструктор
  6. Все остальные функции (статические и нестатические функции-члены, дружественные функции).
  7. Свойства класса (Члены-данные) (статические и нет).

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

Функции

Входные и выходные данные

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

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

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

Параметры функций могут быть входными, выходными или и теми, и другими.
Обязательные входные параметры, как правило, передаются по значению или
константной ссылке. Выходные параметры должны передаваться по неконстантной
ссылке, если они не могут быть null. В общем случае, используйте
std::optional для необязательных входных параметров и константный указатель,
если обязательный параметр был бы ссылкой (поскольку он nullable).
Используйте неконстантные указателя для необязательных выходных или
входных-выходных параметров.

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

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

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

Пишите короткие и концентрированные функции.

Функция должна делать одну вещь и делать ее хорошо! - (c) Дядя Боб.

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

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

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

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

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

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

Например, можно написать функцию, которая принимает аргумент
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.

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

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

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

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

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

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

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

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

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

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

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

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

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

Новый синтаксис возвращаемых значений

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

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

int foo(int x);

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

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

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

Специфическая магия

В данной секции указаны дополнительные рекомендации по повышению надежности
кода на C++.

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

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

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

Умные указатели (smart pointers) - это классы, которые ведут себя как
указатели, то есть, перегружают операторы * и ->. Некоторые умные указатели
могут использоваться для автоматизации управления владением. Шаблон класса
std::unique_ptr - это умный указатель, который отвечает за единоличное
владение: Динамический объект будет уничтожен, когда std::unique_ptr выйдет за
пределы области видимости. Данный тип не копируемый, но переносимый для
осуществления механизма передачи владения от одного объекта к другому. Тип
std::shared_ptr- это умный указатель, который отвечает за совместное владение
динамическим объектом. Объекты этого типа подлежат копированию, а выделенная
память освободится с уничтожением последнего объекта std::shared_ptr.

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

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

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

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

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

Никогда не используйте std::auto_ptr.

Внимание: Qt использует свои умные указатели, например, QSharedPtr,
которые удобно использовать с данной библиотекой. Однако, конвертация из
одного умного указателя в другой невозможна. Старайтесь использовать
std::shared_ptr, где это возможно, для обеспечения единообразия. Если же
проект тесно связан с Qt, можно использовать классы из Qt.
Единообразие здесь - самое важное.

Физический уровень

Утилиты автоматизации

Дополнительные стандарты

Другие особенности С++

Ссылки на rvalue

Используйте rvalue-ссылки (rvalue references) только в некоторых особых
случаях, описанных ниже.

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

Когда суффикс && без дополнительных квалификаторов используется с шаблонным
аргументом функции, то применяются специальные правила вывода типа аргумента.
Такая ссылка имеет название forwarding reference1.

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

  • Ссылки на rvalue все еще не до конца понятны большинству программистов. Такие правила, как сжатие ссылок (reference collapsing) и специальные правила вывода типов данных для forwarding reference остаются неясными.
  • Ссылки на rvalue часто используются неправильно. Использование rvalue-ссылок в сигнатурах, когда аргумент должен оставаться валидным или когда не было выполнения операции std::move, является неинтуитивным.

Не используйте rvalue-ссылки и не используйте квалификатор && в методах,
за исключением следующих случаях:

  • Определение конструкторов перемещения (см. соответствующую секцию).
  • Используйте &&-методы, которые логически "поглощают" *this и оставляют его в неиспользуемом или пустом состоянии. Обратите внимание, что это относится только к квалификаторам методов (после закрывающей скобки в сигнатуре функций). Если необходимо поглотить параметр функции, попробуйте передавать его по значению.
  • Для обеспечения perfect forwarding совместно с std::forward.
  • Для определения пар перегрузок, например foo(const Bar&) и foo(Bar&&). Чаще всего, проще передать параметр по значению, но перегруженная пара функций часто является более производительной или необходима ,когда код должен работать с большим количеством типов данных. Как всегда: если для оптимизации нужно написать более сложный код, то нужно убедиться, что это действительно помогает.

Дружественные функции и классы

Дружественные (friend) функции и классы разрешены, если на это есть веская
причина.

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

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

Исключения

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

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

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

Для того, чтобы обработка исключений была максимально безболезненной,
используйте парадигму RAII и новые механизмы исключений, такие как
std::exception_ptr, std::nested_exception, std::current_exception,
std::rethrow_exception, std::nested_exception и т. д.

Наследуйте классы исключений от std::exception, а еще лучше - от кого-то из
классов-наследников, по типу std::runtime_error и т.п.

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

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

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

Ключевое слово noexcept

Используйте ключевое слово noexcept, если это полезно и корректно.

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

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

  • Спецификация конструктора перемещения как noexcept может повысить производительность в некоторых случаях. Например, std::vector<T>::resize использует семантику перемещения в том случае, если конструктор перемещения для типа T специфицирован как noexcept.
  • Если функция отмечена как noexcept, компилятор может применить дополнительную оптимизацию там, где включены исключения, поскольку не вставляет кода для раскрутки стека (stack unwinding) там, где не будут генерироваться исключения.

  • Тяжело в будущем убрать noexcept из функции, поскольку ее клиенты будут ожидать, что функция noexcept и появление исключения в неожиданном месте может быть сложным для обнаружения.

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

Используйте безусловный noexcept, когда исключения невозможны. В противном
случае используйте условные спецификаторы noexcept с простыми условиями,
которые на выполняются только в очень редких случаях, когда функция может
сгенерировать исключение. Можно, например, использовать проверки
особенностей типов (type traits) или особые условия, когда какая-либо операция
может сгенерировать исключение (std::is_nothrow_move_constructable), или
когда выделение памяти может сгенерировать исключение
(absl::default_allocator_is_nothrow). Скорее всего, если возникла ошибка
аллокации или закончилась память, то это, скорее причина возникновения фатальной
ошибки, чем исключения, от которого программа сможет оправиться. В любом случае,
простота интерфейса приветствуется и, наверное, лучше написать noexcept без
условий, чем описывать очень сложные условия внутри спецификатора noexcept,
если есть причина сделать функцию noexcept.

RTTI

Избегайте использования RTTI.

RTTI (runtime-type information) - это механизм, позволяющий программисту
запрашивать класс объекта во время исполнения. Это делается путем использования
typeid или dynamic_cast.

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

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

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. Как правило, минусы RTTI будут
касаться и этих аналогов.

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

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

В C++ присутствует отличная от C система приведения типов, которая разделяет
различные операции приведения.

Главная проблема с приведением в C - неоднозначность, иногда происходит
конверсия типов (int)3.5, иногда приведение, например (int)"hello".
Инициализация в фигурных скобках и приведение типов из C++ помогают избежать
этой неоднозначности. Также это приведение легко найти в коде поиском или
благодаря подсветке синтаксиса.

Приведение типов на C++ многословно и громоздко.

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

  • Используйте инициализацию в фигурных скобках для арифметических типов. Это самый безопасный способ, поскольку код не компилируется в том случае, если возможна потеря информации. Синтаксис в таком случае выйдет довольно лаконичным.
  • Используйте absl::implicit_cast для безопасного приведения типов вверх по иерархии классов. Например для приведения Foo* к SuperclassOfFoo или const Foo*. Как правило, это делается автоматически компилятором, но иногда нужно делать явно, например, для оператора ?:.
  • Используйте static_cast как эквивалент приведения типов из C, которое конвертирует значение или при необходимости явно преобразовать класс к классу-наследнику. В этом случае вы должны быть уверены, что объект на самом деле является объектом класса-наследника.
  • Используйте const_cast для того, чтобы убрать модификатор const.
  • Используйте reinterpret_cast для небезопасного приведения указателей, включая void*. Используйте это только когда знаете, что делаете. Рассмотрите absl::bit_cast в качестве альтернативы.
  • Используйте absl::bit_cast для приведения "сырых" битов в другой тип того же размера, например, double в int64_t.

В секции бо RTTI описано использование dynamic_cast.

Потоки ввода-вывода

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

Потоки - это стандартный способ ввода/вывода в C++. Они широко используются для
логирования и тестовой диагностики.

Потоки предоставляют простой в освоении API для форматированного ввода-вывода,
который легко портировать и повторно использовать. Для сравнения, например,
printf не поддерживает std::string, не говоря уже о пользовательских типах
данных. Также сложно разрабатывать портируемый код, использующий printf.
Кроме того, printf вынуждает выбирать среди похожих версий одной функции и
ориентироваться в десятках форматных символах.

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


  • Форматирование потоков может изменять состояние потока. Это состояние будет постоянным и влиять на весь последующий ввод-вывод до тех пор, пока вы не будете возвращаться к предыдущему состоянию каждый раз. Более того, пользовательский код может не только модифицировать уже имеющееся состояние, но и вводить новые.
  • Поскольку код и данные перемешаны во время вывода потока, то сложно контролировать, что и как именно будет выведено в поток.
  • Формирование вывода посредством вызова цепочки операторов << затрудняет локализацию, т.к. при этом жёстко фиксируется порядок слов.
  • API потоков сложен в освоении, поэтому программисты должны обладать большим опытом для его грамотного использования.
  • Выборка нужной перегрузки оператора << - затратная для компилятора операция.

Используйте потоковый ввод-вывод только если это лучшее решение проблемы.
Библиотеки для логирования часто лучший выход, чем std::cerr или std::clog,
а библиотеки для работы со строками лучше, чем std::stringstream. Если код
использует Qt, то лучше всего использовать отладочные потоки по типу qDebug и
QString, соответственно. Если нет - найдите подходящую легковесную библиотеку.

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

При использовании потоков избегайте API, которые меняют состояние потоков,
например imbue(), xalloc() или register_callback(). Используйте явные
функции форматирования, например, absl::StreamFormat() вместо потоковых
манипуляторов.

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

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

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

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

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

Раньше везде использовали постфиксный формат в циклах, поэтому он привычнее.

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

Ключевое слово const

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

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

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

Использование const - очень заразное. Если в функцию передается
const-переменная, то в прототипе также должен стоять const или потребуется
const_cast. Это может затруднить использование библиотечных функций.

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

  • Если функция не меняет значение аргумента, передаваемого по значению или по указателю, соответствующий параметр должен быть ссылкой или указателем на константу (const T&/const T*).
  • Если параметр передается по значению, использование const не имеет эффекта и не рекомендуется.
  • Объявляйте методы как const, если они не меняют значения объекта (и не дают такой возможности, например, возвращая неконстантную ссылку) и могут безопасно вызываться из нескольких потоков.

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

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

Где использовать const

Некоторым людям нравится int const* foo вместо const int* foo. Так делать не
надо, const int* foo звучит как словосочетание на английском языке.

Ключевое слово constexpr

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

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

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

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

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

Целочисленные типы данных

Из встроенных целочисленных типов данных C++ следует использовать только int.
Для переменных других типов используйте целочисленные типы данных с точными
размерами из <cstdint>, например, int16_t. Для работы с системой
метаобъектов из библиотеки Qt допустимо использование типов данных из
заголовочного файла <QtGlobal>. Держите в уме, что даже если для результата
требуется меньший размер типа данных, промежуточный результат вычислений может
требовать типа данных побольше. Если сомневаетесь, используйте тип данных
побольше.

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

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

В стандартном заголовочном файле <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, чтобы это гарантировать.

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

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

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

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

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

Переносимость на 64-битные системы

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

  • Переносимое использование printf предполагает использование неприятных и непрактичных макросов (PRI-макросы из <cinttypes>). Не используйте API, которые зависят от семейства printf. Вместо этого используйте безопасное целочисленное форматирование, например StrCat или Substitute из библиотеки absl или std::ostream. > К сожалению, PRI-макросы - это единственный переносимый способ, чтобы
    > указать в printf форматирование для целочисленных типов данных с > точными размерами (например, int64_t, uint64_t, int32_t, uint32_t > и т.д.). Избегайте использование printf. Если невозможно, используйте > типы данных, для которых у 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 C++. При любых ошибках в использовании API
потребуется разбираться в логике макросов; увеличивается время разбора код
инструментами рефакторинга или анализаторами. Как результат, использование
макросов в таких случаях запрещено. Например, откажитесь от подобного кода:

class WOMBAT_TYPE(Foo) {
   // ...
public:
    EXPAND_PUBLIC_WOMBAT_API(Foo)
    EXPAND_WOMBAT_COMPARISONS(Foo, ==, <)
};

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

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

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

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

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

Макросы бывают необходимы при определении заголовочных файлов разделяемых
библиотек в ОС Windows. Например

#ifdef MY_LIBRARY
#define MY_LIBRARY_PUBLIC_API __declspec(dllexport)
#else
#define MY_LIBRARY_PUBLIC_API __declspec(dllimport)
#endif

0, NULL и nullptr

Используйте nullptr для нулевых указателей и \0 для символа конца строки.

Использование nullptr для указателей улучшает безопасность типов.

Использование \0 для символа конца строки делает код более читаемым.

sizeof

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

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

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

Вывод типов данных (в том числе auto)

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

Существует несколько ситуаций, в которых 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 или
как часть ссылки или указателя, но его нельзя использовать как аргумент
шаблона. Более редкий способ использования:
decltype(auto), когда тип выводится из применения decltype к
выражению инициализации.

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

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

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 можно указать названия отдельных элементов вместо названия
целого объекта. Это называется структурная привязка (structured binding), а
сам объявление называется объявлением структурной привязки
(structured binding declaration). Синтаксис не позволяет задать тип ни
полного объекта, ни отдельных имён:

auto [iter, success] = my_map.insert({key, value});
if(!success) {
    iter->second = value;
}

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

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

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

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

auto foo = x.add_foo();
auto i = y.Find(key);

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

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

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

Фундаментальное правило таково: Используйте вывод типов данных только для
того, чтобы сделать код чище и понятнее. Не используйте его только для того,
чтобы избежать неудобства от явного описания типа. Рассуждая о том, будет ли код
чище и понятнее, стоит держать в уме, что читатели кода могут относиться к
другому подразделению или не быть знакомыми с вашим проектом. Если явное
указание типов данных может показаться избыточным для членов вашей команды, оно,
тем не менее, может содержать полезную информацию для других. Например,
возвращаемый тип std::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] = ...

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

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

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

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

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

Компилятор выводит аргументы из выражения инициализации, используя
правила вывода шаблона (template deduction guides), которые могут быть явными
или неявными.

Явные правила вывода выглядят как объявления функции с возвращаемым значением в
конце, но без ключевого слова auto, а название функции такое же, как и
название шаблона. Пример выше опирается на следующие правила вывода:

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

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

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

Иногда 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];
});

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

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

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

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

Тип такой переменной выводится с полным соответствием с правилами использования
auto.

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

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

Вывод:

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

Лучше написать:

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

Шаблонное метапрограммирование

Избегайте сложного шаблонного метапрограммирования.

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

Метапрограммирование позволит создавать очень гибкие, высокопроизводительные и
типобезопасные интерфейсы. Каркасы, как GoogleTest,
std::tuple, std::function, boost::spirit будут невозможны без этого.

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

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

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

Вывод:

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

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

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

Boost

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

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

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

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

Вывод

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

  • boost/call_traits.hpp
  • 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/*).
  • boost/iterator
  • Часть Polygon, которая работает с построением диаграмм Вороного и не зависит от остальной части Polygon: boost/polygon/voronoi_builder.hpp, boost/polygon/voronoi_diagram.hpp, и boost/polygon/voronoi_geometry_type.hpp
  • boost/bimap
  • boost/math/distributions
  • boost/math/special_functions
  • Функции нахождения корня из boost/math/tools
  • boost/multi_index
  • boost/heap
  • flat-контейнеры библиотеки Container: boost/container/flat_map и boost/container/flat_set
  • boost/intrusive
  • boost/sort
  • boost/preprocessor

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

Остальные возможности C++

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

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

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

Псевдонимы (Aliases)

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

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

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

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

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

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

  • Публичные псевдонимы увеличивают количество сущностей в 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

Локальные псевдонимы, созданные для удобства внутри .cpp-файлов, закрытых
секций классов, внутренних пространств имен - это совершенно нормально:

// В .cpp файле
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 = ...;    // Известная аббревиатура полного доменного  
                                   // имени ( Fully Qualified Domain Name)
    }
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 для параметра шаблона.

В дальнейшем будем считать, что "слово" - это всё, что пишется на английском
без пробелов, в том числе и аббревиатуры. Для имён, написанных в смешанном стиле
("camel case" или "Pascal case"), в которых первая буква каждого слова является
заглавной, следует относиться к аббревиатурам как к целому слову. Например,
предпочтительно использовать StartRpc() вместо StartRPC().

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

Имена файлов

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

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

  • my_useful_class.cpp
  • my-useful-class.cpp
  • myusefulclass.cpp
  • myusefulclass_test.cpp - _unittest и _regtest больше не используются.

Файлы C++ должны иметь формат файла .cpp, а заголовочные - .hpp.
Дополнительные файлы, подключаемые как текст, должны именоваться как .inc
(см. секцию
Самодостаточные заголовочные файлы).

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

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

Имена типов

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

Имена всех типов - классов, структур, псевдонимов, перечислений, параметров
шаблонов - именуются в одинаковом стиле.

// классы и структуры
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...

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

// использование псевдонимов
using PropertiesMap = hash_map<UrlTableProperties *, std::string>;

// перечисления
enum UrlTableErrors { ...

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

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

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

Например:

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

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

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

class TableInfo {
    ...
private:
    std::string m_table_name_;  // OK - префикс в начале
    static Pool<TableInfo>* m_pool;  // OK.
};

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

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

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

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

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

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

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

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

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

Обычные функции именуются в смешанном стиле(camel case) (прописные и строчные
буквы); функции доступа к переменным (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.
Для внутренних пространств имён коллизии могут возникать при добавлении другого
кода (внутренние вспомогательные функции имеют свойство повторяться у разных
команд). В этом случае хорошо помогает использование имени файла для именования
пространства имён, например websearch::index::frobber_internal для
использования в frobber.hpp.

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

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

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

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

Комментарии

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

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

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

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

Вы можете использовать либо // либо /* */, однако // - намного

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

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

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

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

Объявление класса должно сопровождаться комментарием, оформленным по правилам
[Doxygen]({TODO:ВСТАВИТЬ_ССЫЛКУ}), и располагаться в заголовочном .hpp-файле.
В случае, если некие тонкости реализации класса заслуживают отдельного
разъяснения, комментарий с разъяснением следует расположить в .cpp файле.

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

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

Дублировать комментарии в обоих файлах не нужно.

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

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

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

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

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

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

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

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

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

Комментарий объявления функции также должен быть оформлен по правилам
[Doxygen]({TODO: Вставить ссылку на Doxygen}).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Если аргумент представляет собой фиксированное значение (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 запросы
).

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

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

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

Длина строк

Длина строки кода не должна превышать 80 символов.

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

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

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

Вывод:

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

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

Не-ASCII символы

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

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

Кодировка hex также допустима, особенно если она улучшает читаемость.
Например, "\xEF\xBB\xBF" или u8"\uFEFF" - неразрывный пробел нулевой длины в
Unicode, и который не должен отображаться в правильном 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).

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

Используйте только пробелы для отступов. Размер отступа составляет 4 пробела.

Мы используем пробелы для отступов. Не используйте табуляцию в своём коде. Вам
стоит настроить свой редактор на вставку 4 пробелов при нажатии клавиши 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,  // Отступ 8 пробелов
        Type par_name2,
        Type par_name3) {
    doSomething();  // Отступ 4 пробела
    ...
}

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

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

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

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;

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

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

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

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

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

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

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

if(...) {
    ...
    ...
    if(...) {
        bool result = doSomething(
                argument1, argument2,  // Отступ 8 пробелов
                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 и else, пробел
ставится только между закрывающей круглой скобкой и открывающей фигурной. Между
круглыми скобками и их содержимым скобки не допускаются. Открывающая фигурная
скобка всегда отделяется пробелом от других конструкций в этой же строке.

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

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

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

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

// Плохо - в конструкции IF присутствует ELSE, а скобки не везде
if(condition)
    foo;
else {
    bar;
}

// Плохо - конструкция IF слишком длинная, чтобы исключить фигурные скобки
if(condition)
    // Comment
    doSomething();

// Плохо - и условие разбито на несколько строк,
// и конструкция длинная, а скобок нет
if(condition1 &&
   condition2)
    DoSomething();

Циклы и switch-case

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

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

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

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

Переход с одной метки на следующую должен быть помечен атрибутом
[[fallthrough]];. Размещайте [[fallthrough]]; в точке, где будет переход.
Исключение из этого правила - последовательные метки без кода, в этом случае
помечать ничего не нужно.

switch(x) {
    case 41:    // Без пометок
    case 43:
        if(dont_be_picky) {
            // Используйте атрибут вместо (или совместно) с комментарием о 
            // переходе
            [[fallthrough]];
        } 
        else {
            closeButNoCigar();
            break;
        }
    case 42:
        doSomethingSpecial();
        [[fallthrough]];
    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;  // Плохо - пробелы с обеих сторон &

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

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

В данном примере, логический оператор && находится всегда в конце строки:

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

Отметим, что разбиение кода (согласно примеру) производится так, чтобы оператор
&& завершал строку. Такой стиль чаще используется в коде 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:         // без отступов
    MyClass();  // Обычный 4-х пробельный отступ
    explicit MyClass(int var);
    ~MyClass() {}

    void someFunction();
    void someFunctionThatDoesNothing() {
    }

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

private:
    bool someInternalFunction();

    int m_some_var;
    int m_some_other_var;
};

Замечания:

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

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

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

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

// Всё в одну строку
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),             // Отступ 8 пробелов
          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 {

Можно использовать новшество из C++17:

namespace foo::bar {
}

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

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

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

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

// Пробелы вокруг двоеточия в списках наследования и инициализации
class Foo : public Bar 
{
public:
    // Для inline-функции добавляйте 
    // пробелы внутри фигурных скобок (кроме пустого блока)
    Foo(int b) : Bar(), m_baz(b) {}    // Пустой блок без пробелов
    void reset() { m_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, включая расширение .cpp для файлов с исходным кодом.
  • Windows определяет собственные синонимы для базовых типов, такие как DWORD, HANDLE и др. Понятно, что при вызове Windows API рекомендуется использовать именно их. И всё равно, старайтесь определять типы, максимально похожие на C++. Например, используйте const TCHAR * вместо LPCTSTR.
  • При компиляции кода с помощью Microsoft Visual C++ установите уровень предупреждений 3 или выше. Также установите настройку, чтобы трактовать все предупреждения как ошибки.
  • Вообще, не используйте нестандартные расширения, такие как #pragma и __declspec (исключение для случаев крайней необходимости). Использование __declspec(dllimport) и __declspec(dllexport) допустимо, однако следует оформить их как макросы DLLIMPORT и DLLEXPORT: в этом случае их можно легко заблокировать, если код будет распространяться.

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

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

  1. Не нашел нормального перевода 

Updated by Redmine Admin over 1 year ago · 1 revisions