Правила оформления стиля кода C++
Предисловие
C++ — один из основных языков разработки, используемых во многих проектах с открытым исходным кодом. Как знает каждый программист на C++, у этого языка много мощных функций, но эта мощь влечет за собой сложность, которая, в свою очередь, может сделать код более подверженным ошибкам и более сложным для чтения и поддержки.
Цель этого руководства — управлять этой сложностью, подробно описывая, что можно и чего нельзя делать при написании кода на C++. Эти правила существуют для того, чтобы кодовая база оставалась управляемой, при этом позволяя программистам продуктивно использовать возможности языка C++.
Стиль, также известный как читаемость, — это то, что мы называем соглашениями, которые управляют нашим кодом на C++. Термин «стиль» немного неточен, поскольку эти соглашения охватывают гораздо больше, чем просто форматирование исходного файла.
Большинство проектов с открытым исходным кодом, разработанных нами, соответствуют требованиям этого руководства.
Обратите внимание, что это руководство не является учебником по C++: мы предполагаем, что читатель знаком с этим языком.
Данное руководство всего лишь рекомендация и не является стандартом оформления стиля кода на языке C++. Как таковых стандартов по оформлению кода вообще не существует, это нужно понимать и учитывать.
Цели руководства по стилю
Зачем нам этот документ?
Есть несколько основных целей, которым, по нашему мнению, должно служить это руководство. Это фундаментальные «почему», лежащие в основе всех отдельных правил. Выдвигая эти идеи на первый план, мы надеемся заложить основу для дискуссий и прояснить для нашего более широкого сообщества, почему существуют правила и почему были приняты определенные решения. Если вы понимаете, каким целям служит каждое правило, всем должно быть понятнее, когда правило может быть отменено (некоторые могут быть отменены), и какой аргумент или альтернатива могут потребоваться для изменения правила в руководстве.
Цели руководства по стилю, как мы их видим в настоящее время, следующие:
Правила стиля должны иметь свой вес: Преимущество правила стиля должно быть достаточно большим, чтобы оправдать просьбу ко всем нашим инженерам запомнить его. Преимущество измеряется относительно кодовой базы, которую мы получили бы без правила, поэтому правило против очень вредной практики все равно может иметь небольшое преимущество, если люди вряд ли будут его делать в любом случае. Этот принцип в основном объясняет правила, которых у нас нет, а не правила, которые у нас есть: например, goto противоречит многим из следующих принципов, но уже исчезающе редок, поэтому в руководстве по стилю он не обсуждается.
Проводите оптимизацию для чтения, а не для записи: Ожидается, что наша кодовая база (и большинство отдельных компонентов, представленных в ней) будет существовать в течение довольно долгого времени. В результате больше времени будет потрачено на чтение большей части нашего кода, чем на его написание. Мы явно выбираем оптимизацию для опыта нашего среднестатистического инженера-программиста, читающего, поддерживающего и отлаживающего код в нашей кодовой базе, а не для удобства при написании этого кода. «Оставьте след для читателя» — особенно распространенный подпункт этого принципа: когда во фрагменте кода происходит что-то удивительное или необычное (например, передача владения указателем), то сохранение текстовых подсказок для читателя в точке использования имеет ценность (std::unique_ptr однозначно демонстрирует передачу владения в месте вызова).
Будьте последовательны с существующим кодом: Последовательное использование одного стиля в нашей кодовой базе позволяет нам сосредоточиться на других (более важных) проблемах. Последовательность также допускает автоматизацию: инструменты, которые форматируют ваш код или корректируют ваши #include, работают правильно только тогда, когда ваш код соответствует ожиданиям инструментария. Во многих случаях правила, которые приписываются «Будьте последовательны», сводятся к «Просто выберите один и перестаньте беспокоиться об этом»; потенциальная ценность предоставления гибкости по этим пунктам перевешивается стоимостью того, что люди спорят о них. Однако существуют пределы последовательности; это хороший решающий фактор, когда нет четких технических аргументов или долгосрочного направления. Он примёняется в большей степени локально (для каждого файла или для тесно связанного набора интерфейсов). Последовательность, как правило, не должна использоваться в качестве оправдания для выполнения действий в старом стиле без учета преимуществ нового стиля или тенденции кодовой базы со временем переходить на новые стили.
Будьте последовательны с более широким сообществом C++, когда это имеет смысл: Последовательность с тем, как другие организации используют C++, имеет ценность по тем же причинам, что и последовательность в нашей кодовой базе. Если функция в стандарте C++ решает проблему или если некоторая идиома широко известна и принята, это аргумент в пользу ее использования. Однако иногда стандартные функции и идиомы имеют недостатки или были просто разработаны без учета потребностей нашей кодовой базы. В таких случаях (как описано ниже) уместно ограничить или запретить стандартные функции. В некоторых случаях мы предпочитаем собственную или стороннюю библиотеку библиотеке, определенной в стандарте C++, либо из-за предполагаемого превосходства, либо из-за недостаточной ценности для перевода кодовой базы на стандартный интерфейс.
Избегайте неожиданных или опасных конструкций: В C++ есть опасные функции. Некоторые ограничения руководства по стилю введены для предотвращения попадания в эти ловушки. Существует высокая планка для отказов от руководства по стилю в отношении таких ограничений, поскольку отказ от таких правил часто напрямую ставит под угрозу корректность программы.
Избегайте конструкций, которые среднестатистический программист на C++ посчитает сложными или трудными для поддержки: C++ имеет функции, которые могут быть неподходящими из-за сложности, которую они привносят в код. В широко используемом коде может быть более приемлемым использовать более сложные языковые конструкции, поскольку любые преимущества более сложной реализации значительно умножаются при использовании, и стоимость понимания сложности не нужно платить снова при работе с новыми частями кодовой базы. В случае сомнений можно запросить отказы от правил такого типа, спросив руководителей вашего проекта. Это особенно важно для нашей кодовой базы, поскольку владение кодом и членство в команде меняются со временем: даже если все, кто работает с каким-то фрагментом кода в настоящее время, понимают его, такое понимание не гарантируется через несколько лет.
Помните о масштабе: С кодовой базой из 100+ миллионов строк и тысячами инженеров некоторые ошибки и упрощения для одного инженера могут дорого обойтись многим. Например, особенно важно избегать загрязнения глобального пространства имён: с конфликтами имён в кодовой базе из сотен миллионов строк трудно работать и их трудно избежать, если все помещают что-то в глобальное пространство имён. Объемный код следует разделять на модули, которые имеют собственные заголовочные файлы и собственную реализацию. По возможности следует избегать, реализации кодовой базы в заголовочных файлах. В рамках проекта, должнен использоваться индивидуальный namespace который не будет пересекаться с другими проектами, это позволит использовать более простые и коротки названия переменных и классов.
При необходимости прибегайте к оптимизации: Иногда оптимизация производительности может быть необходимой и уместной, даже если она противоречит другим принципам этого документа.
Цель этого документа — предоставить максимальное руководство с разумными ограничениями. Как всегда, здравый смысл и хороший вкус должны преобладать. Под этим мы конкретно подразумеваем устоявшиеся соглашения всего сообщества C++, а не только ваши личные предпочтения или предпочтения вашей команды. Будьте скептически настроены и неохотно используйте умные или необычные конструкции: отсутствие запрета не то же самое, что лицензия на продолжение. Используйте свое суждение, и если вы не уверены, пожалуйста, не стесняйтесь просить руководителей ваших проектов получить дополнительную информацию.
Версия C++
В настоящее время код должен быть нацелен на С++17 или C++20, т.е. не должен использовать функции C++23. Версия C++, на которую нацелено это руководство, будет развиваться (агрессивно) со временем.
Не используйте нестандартные расширения.
Рассмотрите возможность переносимости в другие среды перед использованием функций из C++17 и C++20 в вашем проекте.
Заголовочные файлы
В общем, каждый файл .cpp должен иметь связанный файл .hpp. Существуют некоторые общие исключения, такие как модульные тесты и небольшие файлы .cpp, содержащие только функцию main().
Правильное использование заголовочных файлов может иметь огромное значение для читаемости, размера и производительности вашего кода.
Следующие правила проведут вас через различные подводные камни использования заголовочных файлов.
Автономные заголовки
Заголовочные файлы должны быть самодостаточными (компилироваться самостоятельно) и заканчиваться на .hpp. Незаголовочные файлы, предназначенные для включения, должны заканчиваться на .inc и использоваться экономно.
Все заголовочные файлы должны быть самодостаточными. Пользователи и инструменты рефакторинга не должны придерживаться особых условий для включения заголовка. В частности, заголовок должен иметь защиту заголовка и включать все другие необходимые заголовки.
Когда заголовок объявляет встроенные функции или шаблоны, которые будут инстанцировать клиенты заголовка, встроенные функции и шаблоны также должны иметь определения в заголовке, либо напрямую, либо во включаемых файлах. Не перемещайте эти определения в отдельно включенные заголовочные файлы (-inl.hpp); такая практика была распространена в прошлом, но теперь не допускается. Когда все инстанцирования шаблона происходят в одном файле .cpp, либо потому, что они явные, либо потому, что определение доступно только для файла .cpp, определение шаблона можно сохранить в этом файле.
Редко бывает так, что файл, предназначенный для включения, не является самодостаточным. Обычно они предназначены для включения в необычные места, например, в середину другого файла. Они могут не использовать защитные заголовки и не включать свои предварительные условия. Называйте такие файлы расширением .inc. Используйте умеренно и предпочитайте самодостаточные заголовки, когда это возможно.
#define Guard
Все файлы заголовков должны иметь защиту #define для предотвращения множественного включения. Формат имёни символа должен быть _.
Чтобы гарантировать уникальность, они должны основываться на уникальном названии модуля. Например, файл baz.hpp в проекте foo должен иметь следующую защиту:
#ifndef __FOO_BAZ__
#define __FOO_BAZ__
...
#endif // __FOO_BAZ__
Предварительные декларации
Избегайте использования предварительных деклараций, где это возможно. Вместо этого включайте нужные заголовки.
Определение: «Предварительная декларация» — это декларация сущности без связанного определения.
// В C++ файле реализации
class B;
void FuncInB();
extern int variable_in_b;
ABSL_DECLARE_FLAG(flag_in_b);
Плюсы:
- Предварительные объявления могут сэкономить время компиляции, поскольку #includes заставляют компилятор открывать больше файлов и обрабатывать больше входных данных.
- Предварительные объявления могут сэкономить на ненужной перекомпиляции. #includes могут заставить ваш код перекомпилироваться чаще из-за несвязанных изменений в заголовке.
Минусы:
- Прямые объявления могут скрыть зависимость, позволяя пользовательскому коду пропускать необходимую перекомпиляцию при изменении заголовков.
- Прямые объявления в отличие от оператора #include затрудняют автоматическому инструментированию обнаружение модуля, определяющего символ.
- Прямые объявления могут быть нарушены последующими изменениями в библиотеке. Прямые объявления функций и шаблонов могут помешать владельцам заголовков вносить в свои 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);
} // Вызов f(B*)
Если бы #include был заменен на прямые объявления для B и D, test() вызвал бы f(void *).
- Прямое объявление нескольких символов из заголовка может быть более многословным, чем простое #includeing заголовка.
- Структурирование кода для включения прямых объявлений (например, использование членов указателей вместо членов объектов) может сделать код медленнее и сложнее.
Решение:
Постарайтесь избегать предварительных объявлений сущностей, определенных в другом проекте.
Встроенные функции
Определяйте функции как встроенные только в том случае, если они небольшие, скажем, 10 строк или меньше. Или если реализация данных функций больше нигде не используется.
Определение:
Вы можете объявлять функции таким образом, чтобы компилятор мог расширять их в строке, а не вызывать их через обычный механизм вызова функций.
Плюсы:
Встраивание функции может генерировать более эффективный объектный код, если встраиваемая функция небольшая. Не стесняйтесь встраивать аксессоры и мутаторы, а также другие короткие, критически важные для производительности функции.
Минусы:
Злоупотребление встраиванием может фактически замедлить работу программ. В зависимости от размера функции, встраивание может привести к увеличению или уменьшению размера кода. Встраивание очень маленькой функции доступа обычно уменьшает размер кода, в то время как встраивание очень большой функции может значительно увеличить размер кода. На современных процессорах меньший код обычно выполняется быстрее из-за лучшего использования кэша инструкций.
Решение:
Хорошее практическое правило — не встраивать функцию, если ее длина превышает 10 строк. Остерегайтесь деструкторов, которые часто длиннее, чем кажутся, из-за неявных вызовов деструкторов членов и базовых деструкторов!
Другое полезное практическое правило: обычно невыгодно встраивать функции с циклами или операторами switch (если только, в общем случае, оператор цикла или switch никогда не выполняется).
Важно знать, что функции не всегда встраиваются, даже если они объявлены как таковые; например, виртуальные и рекурсивные функции обычно не встраиваются. Обычно рекурсивные функции не должны быть встраиваемыми. Основная причина встраивания виртуальной функции — поместить ее определение в класс, либо для удобства, либо для документирования ее поведения, например, для методов доступа и мутаторов.
Имёна и порядок включения
Включите заголовки в следующем порядке: связанный заголовок, заголовки системы C, заголовки стандартной библиотеки C++, заголовки других библиотек, заголовки вашего проекта.
Все файлы заголовков проекта должны быть перечислены как потомки исходного каталога проекта без использования псевдонимов каталогов UNIX. (текущий каталог) или .. (родительский каталог). Например, awesome-project/src/base/logging.hpp следует включить как:
#include "base/logging.hpp"
Заголовки следует включать с использованием пути в угловых скобках только в том случае, если библиотека требует этого. В частности, следующие заголовки требуют угловых скобок:
- Заголовочные файлы стандартных библиотек C и C++ (например, <stdlib.h> и ).
- Заголовочные файлы систем POSIX, Linux и Windows (например, <unistd.h> и <windows.h>).
- В редких случаях сторонние библиотеки (например, <Python.h>).
В dir/foo.cpp или dir/foo_test.cpp, чьей основной целью является реализация или тестирование содержимого dir2/foo2.hpp, упорядочите ваши включения следующим образом:
- dir2/foo2.hpp.
- Пустая строка
- Заголовочные файлы системы C и любые другие заголовочные файлы в угловых скобках с расширением .hpp или .h, например, <unistd.h>, <stdlib.h>, <Python.h>.
- Пустая строка
- Заголовочные файлы стандартной библиотеки C++ (без расширения файла), например, , .
- Пустая строка
- Файлы .hpp других библиотек.
- Пустая строка
- Файлы .hpp вашего проекта.
Разделяйте каждую непустую группу одной пустой строкой.
При предпочтительном порядке, если связанный заголовок dir2/foo2.hpp не содержит какие-либо необходимые включения, сборка dir/foo.cpp или dir/foo_test.cpp будет нарушена. Таким образом, это правило гарантирует, что прерывания сборки сначала появятся для людей, работающих над этими файлами, а не для людей работающих в других пакетах.
dir/foo.cpp и dir2/foo2.hpp обычно находятся в разных каталогах (например, src/basictypes_test.cpp и include/basictypes.hpp), но иногда могут находиться и в одном каталоге.
Обратите внимание, что заголовки C, такие как stddef.h, по сути, взаимозаменяемы со своими аналогами C++ (cstddef). Любой стиль приемлем, но предпочтительнее согласованность с существующим кодом.
В каждом разделе включения должны быть упорядочены в порядке размера названия, от наимёньшего размера названия к наибольшему. Обратите внимание, что старый код может не соответствовать этому правилу и должен быть исправлен, когда это будет удобно.
Например, включения в awesome-project/src/foo/internal/fooserver.cpp могут выглядеть следующим образом:
#include <string>
#include <vector>
#include <unistd.h>
#include <sys/types.h>
#include "foo/server/bar.hpp"
#include "base/basictypes.hpp"
#include "foo/server/fooserver.hpp"
#include "third_party/absl/flags/flag.hpp"
Исключение:
Иногда системно-специфический код нуждается в условных включениях. Такой код может помещать условные включения после других включений. Конечно, ваш системно-специфический код должен быть небольшим и локализованным. Пример:
#ifdef LANG_CXX11
#include <initializer_list>
#endif // LANG_CXX11
// For LANG_CXX11.
#include "base/port.hpp"
#include "foo/public/fooserver.hpp"
Область примёнения
Пространства имён
За редкими исключениями размещайте код в пространстве имён. Пространства имён должны иметь уникальные имёна, основанные на имёни проекта и, возможно, его пути. Не используйте директивы using (например, using namespace foo). Не используйте встроенные пространства имён. Для неимёнованных пространств имён см. Внутреннее связывание.
Определение:
Пространства имён подразделяют глобальную область действия на отдельные имёнованные области действия и поэтому полезны для предотвращения конфликтов имён в глобальной области действия.
Плюсы:
Пространства имён предоставляют метод предотвращения конфликтов имён в больших программах, позволяя большинству кода использовать достаточно короткие имёна.
Например, если два разных проекта имеют класс Foo в глобальной области видимости, эти символы могут конфликтовать во время компиляции или во время выполнения. Если каждый проект помещает свой код в пространство имён, project1::Foo и project2::Foo теперь являются отдельными символами, которые не конфликтуют, и код в пространстве имён каждого проекта может продолжать ссылаться на Foo без префикса.
Встроенные пространства имён автоматически помещают свои имёна в окружающую область видимости. Рассмотрим, например, следующий фрагмент:
namespace outer {
inline namespace inner {
void foo();
}; // namespace inner
}; // namespace outer
Выражения outer::inner::foo() и outer::foo() взаимозаменяемы. Встроенные пространства имён в первую очередь предназначены для совместимости ABI между версиями.
Минусы:
Пространства имён могут сбивать с толку, поскольку они усложняют механизм выяснения того, к какому определению относится имя.
Встроенные пространства имён, в частности, могут сбивать с толку, поскольку имёна фактически не ограничены пространством имён, в котором они объявлены. Они полезны только как часть более крупной политики управления версиями.
В некоторых контекстах необходимо неоднократно ссылаться на символы по их полностью определенным имёнам. Для глубоко вложенных пространств имён это может добавить много беспорядка.
Решение:
Пространства имён следует использовать следующим образом:
- Следуйте правилам имён пространств имён.
- Завершайте многострочные пространства имён комментариями, как показано в приведенных примерах.
- Пространства имён охватывают весь исходный файл после включений, определений/деклараций gflags и предварительных деклараций классов из других пространств имён.
// В файле .hpp
namespace mynamespace {
// Все объявления находятся в пределах области действия пространства имён.
// Обратите внимание на отсутствие отступов.
class MyClass {
public:
...
void Foo();
};
} // namespace mynamespace
// В файле .cpp
// Определение функций находится в пределах пространства имён.
void MyClass::Foo() {
...
}
Более сложные файлы .cpp могут содержать дополнительные сведения, такие как флаги или объявления об использовании.
#include "a.hpp"
ABSL_FLAG(bool, someflag, false, "a flag");
namespace mynamespace {
using ::foo::Bar;
// Код выходит за пределы левого поля.
...code for mynamespace...
}; // namespace mynamespace
- Чтобы поместить сгенерированный код сообщения протокола в пространство имён, используйте спецификатор пакета в файле .proto. Подробности см. в разделе Пакеты буфера протокола.
- Не объявляйте ничего в пространстве имён std, включая предварительные объявления классов стандартной библиотеки. Объявление сущностей в пространстве имён std является неопределённым поведением, т.е. непереносимым. Чтобы объявить сущности из стандартной библиотеки, включите соответствующий заголовочный файл.
- Вы не можете использовать директиву using, чтобы сделать все имёна из пространства имён доступными.
// Запрещено — это загрязняет пространство имён.
using namespace foo;
- Не используйте псевдонимы пространств имён в области действия пространства имён в заголовочных файлах, за исключением явно обозначенных внутренних пространств имён, поскольку все, что импортируется в пространство имён в заголовочном файле, становится частью публичного API, экспортируемого этим файлом.
// Сократить доступ к некоторым часто используемым именам в файлах .cpp.
namespace baz = ::foo::bar::baz;
// Сократить доступ к некоторым часто используемым именам (в файле .hpp).
namespace librarian {
namespace internal { // Внутренний, не является частью API.
namespace sidetable = ::pipeline_diagnostics::sidetable;
}; // namespace internal
inline void my_inline_function(){
// псевдоним пространства имён, локальный для функции (или метода).
namespace baz = ::foo::bar::baz;
...
}
}; // namespace librarian
Разрешается использование using namespace в файлах .cpp без ограничения, в заголовочных файлах, использовать using namespace разрешено только под собственным уникальным namespace текущего проекта.
namespace somespace {
using namespace std;
class ...
};
- Не используйте встроенные пространства имён.
- Используйте пространства имён со словом «internal» в названии для документирования частей API, которые не должны упоминаться пользователями API.
// Мы не должны использовать это внутреннее имя в не-ABSL-коде.
using ::absl::container_internal::ImplementationDetail;
- В новом коде предпочтительны однострочные вложенные объявления пространств имён, но они не являются обязательными.
Внутренние связи
Когда определения в файле .cpp не нуждаются в ссылках за пределами этого файла, передайте им внутреннюю связь, поместив их в безымянное пространство имён или объявив их статическими. Не используйте ни одну из этих конструкций в файлах .hpp.
Определение:
Все объявления могут быть связаны внутри, если поместить их в неименованные пространства имён. Функции и переменные также могут быть связаны внутри, если объявить их статическими. Это означает, что всё, что вы объявляете, не может быть доступно из другого файла. Если другой файл объявляет что-то с тем же именем, то эти две сущности полностью независимы.
Решение:
Использование внутренних связей в файлах .cpp рекомендуется для всего кода, на который не нужно ссылаться в другом месте. Не используйте внутренние связи в файлах .hpp.
Форматируйте неименованные пространства имён как именованные пространства имён. В завершающем комментарии оставьте имя пространства имён пустым:
namespace {
...
}; // namespace
Нечлены, статические члены и глобальные функции
Предпочитайте размещать функции, не являющиеся членами, в пространстве имён; используйте полностью глобальные функции редко. Не используйте класс просто для группировки статических членов. Статические методы класса должны быть, как правило, тесно связаны с экземплярами класса или статическими данными класса.
Плюсы:
Нечленские и статические функции-члены могут быть полезны в некоторых ситуациях. Размещение нечленских функций в пространстве имён позволяет избежать загрязнения глобального пространства имён.
Минусы:
Статические функции-члены и функции-нечлены могут иметь больше смысла в качестве членов нового класса, особенно если они обращаются к внешним ресурсам или имеют существенные зависимости.
Решение:
Иногда полезно определить функцию, не привязанную к экземпляру класса. Такая функция может быть как статическим членом, так и функцией-нечленом. Функции-нечлены не должны зависеть от внешних переменных и должны почти всегда существовать в пространстве имён. Не создавайте классы только для группировки статических членов; это ничем не отличается от простого присвоения именам общего префикса, и такая группировка обычно в любом случае не нужна.
Если вы определяете функцию-нечлен, и она нужна только в ее файле .cpp, используйте внутреннюю компоновку, чтобы ограничить её область действия.
Локальные переменные
Поместите переменные функции в максимально узкую область действия и инициализируйте переменные в объявлении.
C++ позволяет объявлять переменные в любом месте функции. Мы рекомендуем вам объявлять их в области действия как можно более локальной и как можно ближе к первому использованию. Это упрощает читателю поиск объявления и просмотр типа переменной и того, как она была инициализирована. В частности, вместо объявления и присваивания следует использовать инициализацию, например:
int32_t i;
i = f(); // Плохо — инициализация отделена от объявления.
int32_t i = f(); // Хорошо — объявление имеет инициализацию.
int32_t jobs = NumJobs();
// Больше кода...
f(jobs); // Плохо — объявление отделено от использования.
int32_t jobs = NumJobs();
f(jobs); // Хорошо — объявление сразу (или почти сразу) следует за использованием.
std::vector <int32_t> v;
v.push_back(1); // Предпочитайте инициализацию с использованием фигурных скобок.
v.push_back(2);
std::vector <int32_t> v = {1, 2}; // Хорошо — v начинает инициализацию.
Переменные, необходимые для операторов if, while и for, обычно должны объявляться внутри этих операторов, чтобы такие переменные были ограничены этими областями действия. Например:
while(const char * p = strchr(str, '/'))
str = p + 1;
Есть одно предостережение: если переменная является объектом, её конструктор вызывается каждый раз, когда она входит в область видимости и создаётся, а её деструктор вызывается каждый раз, когда она выходит из области видимости.
// Неэффективная реализация:
for(int32_t i = 0; i < 1000000; ++i){
Foo f; // Мои ctor и dtor вызываются по 1000000 раз каждый.
f.DoSomething(i);
}
Возможно, будет эффективнее объявить такую переменную, используемую в цикле, вне этого цикла:
Foo f; // Мои ctor и dtor вызываются по одному разу.
for(int32_t i = 0; i < 1000000; ++i)
f.DoSomething(i);
Статически и глобальные переменные
Объекты со статической продолжительностью хранения запрещены, если только они не являются тривиально разрушаемыми. Неформально это означает, что деструктор ничего не делает, даже принимая во внимание деструкторы элементов и баз. Более формально это означает, что тип не имеет определяемого пользователем или виртуального деструктора и что все базовые и нестатические элементы являются тривиально разрушаемыми. Статические локальные переменные функции могут использовать динамическую инициализацию. Использование динамической инициализации для статических переменных-членов класса или переменных в области видимости пространства имён не рекомендуется, но допускается в ограниченных обстоятельствах; подробности см. ниже.
Как правило: глобальная переменная удовлетворяет этим требованиям, если её объявление, рассматриваемое изолированно, может быть constexpr.
Определение:
Каждый объект имеет длительность хранения, которая коррелирует с его временем жизни. Объекты со статической длительностью хранения существуют с момента их инициализации до конца программы. Такие объекты появляются как переменные в области видимости пространства имён («глобальные переменные»), как статические члены данных классов или как локальные переменные функций, которые объявляются со спецификатором static. Локальные статические переменные функций инициализируются, когда управление впервые проходит через их объявление; все остальные объекты со статической длительностью хранения инициализируются как часть запуска программы. Все объекты со статической длительностью хранения уничтожаются при выходе из программы (что происходит до завершения необъединенных потоков).
Инициализация может быть динамической, что означает, что во время инициализации происходит что-то нетривиальное. (Например, рассмотрим конструктор, который выделяет память, или переменную, которая инициализируется текущим идентификатором процесса.) Другой вид инициализации — статическая инициализация. Однако эти два процесса не являются полной противоположностью: статическая инициализация всегда происходит с объектами со статической продолжительностью хранения (инициализация объекта либо заданной константой, либо представлением, состоящим из всех байтов, установленных в ноль), тогда как динамическая инициализация происходит после этого, если это необходимо.
Плюсы:
Глобальные и статические переменные очень полезны для большого количества приложений: именованные константы, вспомогательные структуры данных, внутренние для некоторых единиц трансляции, флаги командной строки, протоколирование, механизмы регистрации, фоновая инфраструктура и т.д.
Минусы:
Глобальные и статические переменные, которые используют динамическую инициализацию или имеют нетривиальные деструкторы, создают сложность, которая может легко привести к труднообнаружимым ошибкам. Динамическая инициализация не упорядочена по единицам трансляции, как и уничтожение (за исключением того, что уничтожение происходит в обратном порядке инициализации). Когда одна инициализация ссылается на другую переменную со статической продолжительностью хранения, возможно, что это приведет к доступу к объекту до начала его жизненного цикла (или после окончания его жизненного цикла). Более того, когда программа запускает потоки, которые не объединены на выходе, эти потоки могут попытаться получить доступ к объектам после окончания их жизненного цикла, если их деструктор уже запущен.
Решение:
Решение об уничтожении
Когда деструкторы тривиальны, их выполнение вообще не подлежит упорядочиванию (они фактически не «запускаются»); в противном случае мы подвергаемся риску доступа к объектам после окончания их жизненного цикла. Поэтому мы разрешаем объекты со статической продолжительностью хранения только в том случае, если они тривиально разрушаемы. Фундаментальные типы (такие как указатели и int) тривиально разрушаемы, как и массивы тривиально разрушаемых типов. Обратите внимание, что переменные, отмеченные constexpr, тривиально разрушаемы.
const int32_t kNum = 10; // Разрешено
struct X {
int32_t n;
};
const X kX[] = {{1}, {2}, {3}}; // Разрешено
void foo(){
static const char * const kMessages[] = {"hello", "world"}; // Разрешено
}
// Разрешено: constexpr гарантирует тривиальный деструктор.
constexpr std::array <int32_t, 3> kArray = {1, 2, 3};
// плохо: нетривиальный деструктор
const std::string kFoo = "foo";
// Плохо по той же причине, даже несмотря на то, что kBar является ссылкой (правило также применяется к временным объектам с продленным сроком существования).
const std::string & kBar = StrCat("a", "b", "c");
void bar(){
// Плохо: нетривиальный деструктор.
static std::map <int32_t, int32_t> kData = {{1,0}, {2,0}, {3,0}};
}
Обратите внимание, что ссылки не являются объектами, и поэтому они не подчиняются ограничениям на разрушаемость. Однако ограничение на динамическую инициализацию всё ещё применяется. В частности, разрешена локальная для функции статическая ссылка вида static T & t = * new T;.
Решение об инициализации
Инициализация — более сложная тема. Это связано с тем, что мы должны не только учитывать, выполняются ли конструкторы классов, но и учитывать оценку инициализатора:
int32_t n = 5; // Отлично
int32_t m = f(); // ? (Зависимость от f)
Foo x; // ? (Зависимость от Foo::Foo)
Bar y = g(); // ? (Зависимость от g и от Bar::Bar)
Всё, кроме первого оператора, подвергают нас неопределенному порядку инициализации.
Концепция, которую мы ищем, называется константной инициализацией на формальном языке стандарта C++. Это означает, что инициализирующее выражение является константным выражением, и если объект инициализируется вызовом конструктора, то конструктор также должен быть указан как constexpr:
struct Foo {
constexpr Foo(int32_t){}
};
int32_t n = 5; // Хорошо, 5 — это постоянное выражение.
Foo x(2); // Хорошо, 2 — это константное выражение, а выбранный конструктор — constexpr.
Foo a[] = {
Foo(1), Foo(2), Foo(3)
}; // Fine
Константная инициализация всегда разрешена. Константная инициализация статических переменных длительности хранения должна быть отмечена как constexpr или constinit. Любая нелокальная статическая переменная длительности хранения, которая не отмечена таким образом, должна считаться имеющей динамическую инициализацию и рассматриваться очень внимательно.
Напротив, следующие инициализации являются проблематичными:
// Некоторые декларации, используемые ниже.
time_t time(time_t *); // Не constexpr!
int32_t f(); // Не constexpr!
struct Bar {
Bar(){}
};
// Проблемные инициализации.
time_t m = ::time(nullptr); // Инициализирующее выражение не является константным выражением.
Foo y(f()); // То же самое
Bar b; // Выбранный конструктор Bar::Bar() не constexpr.
Динамическая инициализация нелокальных переменных не приветствуется и, в общем, запрещена. Однако мы разрешаем её, если ни один аспект программы не зависит от последовательности этой инициализации по отношению ко всем другим инициализациям. При таких ограничениях порядок инициализации не имеет заметного значения. Например:
// Разрешено, если никакая другая статическая переменная не использует p в своей инициализации.
int32_t p = ::getpid();
Динамическая инициализация статических локальных переменных разрешена (и является общепринятой).
Общие закономерности
- Глобальные строки: если вам требуется именованная глобальная или статическая строковая константа, рассмотрите возможность использования переменной constexpr string_view, массива символов или указателя символов, указывающей на строковый литерал. Строковые литералы уже имеют статическую продолжительность хранения и обычно достаточны. Карты, наборы и другие динамические контейнеры: если вам требуется статическая фиксированная коллекция, такая как набор для поиска или таблица поиска, вы не можете использовать динамические контейнеры из стандартной библиотеки в качестве статической переменной, поскольку у них есть нетривиальные деструкторы. Вместо этого рассмотрите простой массив тривиальных типов, например, массив массивов целых чисел (для «отображения из int в int») или массив пар (например, пар int и const char *). Для небольших коллекций линейный поиск вполне достаточен (и эффективен из-за локальности памяти); рассмотрите возможность использования средств из absl/algorithm/container.hpp для стандартных операций. При необходимости храните коллекцию в отсортированном порядке и используйте алгоритм бинарного поиска. Если вы действительно предпочитаете динамический контейнер из стандартной библиотеки, рассмотрите возможность использования статического указателя, локального для функции, как описано ниже.
- Умные указатели (std::unique_ptr, std::shared_ptr): умные указатели выполняют очистку во время уничтожения и поэтому запрещены. Подумайте, соответствует ли ваш вариант использования одному из других шаблонов, описанных в этом разделе. Одним из простых решений является использование простого указателя на динамически выделенный объект и никогда не удаляйте его (см. последний пункт).
- Статические переменные пользовательских типов: если вам требуются статические, постоянные данные типа, который вам нужно определить самостоятельно, дайте типу тривиальный деструктор и конструктор constexpr.
- Если все остальное не помогает, вы можете создать объект динамически и никогда не удалять его, используя статический указатель или ссылку, локального для функции (например, static const auto & impl = * new T(args…);).
thread_local Переменные
Переменные thread_local, которые не объявлены внутри функции, должны быть инициализированы истинной константой времени компиляции, и это должно быть обеспечено с помощью атрибута constinit. Предпочитайте thread_local другим способам определения локальных данных потока.
Определение:
Переменные можно объявлять с помощью спецификатора thread_local:
thread_local Foo foo = ...;
Такая переменная на самом деле является коллекцией объектов, так что когда разные потоки обращаются к ней, они на самом деле обращаются к разным объектам. Переменные thread_local во многом похожи на статические переменные длительности хранения. Например, их можно объявлять в области видимости пространства имён, внутри функций или как статические члены класса, но не как обычные члены класса.
Экземпляры переменных thread_local инициализируются во многом так же, как статические переменные, за исключением того, что они должны быть инициализированы отдельно для каждого потока, а не один раз при запуске программы. Это означает, что переменные thread_local, объявленные внутри функции, безопасны, но другие переменные thread_local подвержены тем же проблемам порядка инициализации, что и статические переменные (и многим другим).
Переменные thread_local имеют тонкую проблему порядка уничтожения: во время завершения работы потока переменные thread_local будут уничтожены в порядке, противоположном их инициализации (как это обычно бывает в C++). Если код, запущенный деструктором любой переменной thread_local, ссылается на любую уже уничтоженную переменную thread_local в этом потоке, мы получим особенно трудно диагностируемую ошибку использования после освобождения.
Плюсы:
- Локальные данные потока по своей сути защищены от гонок (потому что обычно к ним может получить доступ только один поток), что делает thread_local полезным для параллельного программирования.
- thread_local — единственный поддерживаемый стандартом способ создания локальных данных потока.
Минусы:
- Доступ к переменной thread_local может вызвать выполнение непредсказуемого и неконтролируемого количества другого кода во время запуска потока или первого использования в данном потоке.
- Переменные thread_local фактически являются глобальными переменными и имеют все недостатки глобальных переменных, за исключением отсутствия потокобезопасности.
- Память, потребляемая переменной thread_local, масштабируется с числом запущенных потоков (в худшем случае), которое может быть довольно большим в программе.
- Элементы данных не могут быть thread_local, если они также не являются статическими.
- Мы можем страдать от ошибок использования после освобождения, если переменные thread_local имеют сложные деструкторы. В частности, деструктор любой такой переменной не должен вызывать какой-либо код (транзитивно), который ссылается на любой потенциально уничтоженный thread_local. Это свойство трудно реализовать.
- Подходы к избежанию использования после освобождения в глобальных/статических контекстах не работают для thread_locals. В частности, пропуск деструкторов для глобальных и статических переменных допустим, поскольку их время жизни заканчивается при завершении работы программы. Таким образом, любая «утечка» немедленно устраняется ОС, очищающей нашу память и другие ресурсы. Напротив, пропуск деструкторов для переменных thread_local приводит к утечкам ресурсов, пропорциональным общему числу потоков, которые завершаются в течение жизненного цикла программы.
Решение:
Переменные thread_local в области класса или пространства имен должны быть инициализированы истинной константой времени компиляции (т.е. они не должны иметь динамической инициализации). Чтобы обеспечить это, переменные thread_local в области класса или пространства имен должны быть аннотированы constinit (или constexpr, но это должно быть редко):
constinit thread_local Foo foo = ...;
Переменные thread_local внутри функции не имеют проблем с инициализацией, но всё ещё рискуют использовать после освобождения во время выхода из потока. Обратите внимание, что вы можете использовать thread_local в области функции для имитации thread_local в области класса или пространства имён, определив функцию или статический метод, который ее раскрывает:
Foo & MyThreadLocalFoo(){
thread_local Foo result = ComplicatedInitialization();
return result;
}
Обратите внимание, что переменные thread_local будут уничтожены при каждом выходе из потока. Если деструктор любой такой переменной ссылается на любую другую (потенциально уничтоженную) thread_local, мы будем страдать от трудно диагностируемых ошибок использования после освобождения. Предпочитайте тривиальные типы или типы, которые доказуемо не запускают предоставленный пользователем код при уничтожении, чтобы минимизировать потенциальный доступ к любой другой thread_local.
thread_local следует предпочесть другим механизмам определения локальных данных потока.
Классы
Классы являются фундаментальной единицей кода в C++. Естественно, мы используем их широко. В этом разделе перечислены основные правила, которым следует следовать при написании класса.
Выполнение работы в конструкторах
Избегайте вызовов виртуальных методов в конструкторах и инициализации, которая может завершиться неудачей, если вы не сможете сообщить об ошибке.
Определение:
В теле конструктора можно выполнить произвольную инициализацию.
Плюсы:
- Не нужно беспокоиться о том, был ли класс инициализирован или нет.
- Объекты, которые полностью инициализируются вызовом конструктора, могут быть константными и также могут быть проще в использовании со стандартными контейнерами или алгоритмами.
Минусы:
- Если работа вызывает виртуальные функции, эти вызовы не будут отправлены реализациям подкласса. Будущие изменения вашего класса могут незаметно ввести эту проблему, даже если ваш класс в настоящее время не является подклассом, что вызовет много путаницы.
- У конструкторов нет простого способа сообщать об ошибках, за исключением сбоя программы (не всегда уместно) или использования исключений (которые запрещены).
- Если работа не удается, у нас теперь есть объект, код инициализации которого не удался, поэтому это может быть необычное состояние, требующее механизма проверки состояния bool IsValid() (или аналогичного), который легко забыть вызвать.
- Вы не можете взять адрес конструктора, поэтому любую работу, выполняемую в конструкторе, нельзя легко передать, например, другому потоку.
Решение:
Конструкторы никогда не должны вызывать виртуальные функции. Если это подходит для вашего кода, завершение программы может быть подходящим ответом обработки ошибок. В противном случае рассмотрите функцию-фабрику или метод Init(). Избегайте методов Init() для объектов без других состояний, которые влияют на то, какие публичные методы могут быть вызваны (с полусконструированными объектами этой формы особенно сложно работать правильно).
Неявные преобразования
Не определяйте неявные преобразования. Используйте ключевое слово explicit для операторов преобразования и конструкторов с одним аргументом.
Определение:
Неявные преобразования позволяют использовать объект одного типа (называемый исходным типом) там, где ожидается другой тип (называемый целевым типом), например, при передаче аргумента int в функцию, которая принимает параметр double.
В дополнение к неявным преобразованиям, определенным языком, пользователи могут определять свои собственные, добавляя соответствующие члены в определение класса исходного или целевого типа. Неявное преобразование в исходном типе определяется оператором преобразования типа, названным в честь целевого типа (например, оператор bool()). Неявное преобразование в целевом типе определяется конструктором, который может принимать исходный тип в качестве своего единственного аргумента (или единственного аргумента без значения по умолчанию).
Ключевое слово explicit может применяться к конструктору или оператору преобразования, чтобы гарантировать, что его можно использовать только тогда, когда целевой тип явный в точке использования, например, с приведением. Это относится не только к неявным преобразованиям, но и к синтаксису инициализации списка:
class Foo {
explicit Foo(int32_t x, double y);
...
};
void Func(Foo f);
Func({42, 3.14}); // Ошибка!
Этот тип кода технически не является неявным преобразованием, но язык рассматривает его как явное преобразование.
Плюсы:
- Неявные преобразования могут сделать тип более удобным и понятным, устраняя необходимость явно называть тип, когда это очевидно.
- Неявные преобразования могут быть более простой альтернативой перегрузке, например, когда одна функция с параметром string_view заменяет отдельные перегрузки для std::string и const char *.
- Синтаксис инициализации списка — это краткий и выразительный способ инициализации объектов.
Минусы:
- Неявные преобразования могут скрывать ошибки несоответствия типов, когда целевой тип не соответствует ожиданиям пользователя, или пользователь не знает, что какое-либо преобразование будет иметь место.
- Неявные преобразования могут сделать код более трудным для чтения, особенно при наличии перегрузки, делая менее очевидным, какой код на самом деле вызывается.
- Конструкторы, которые принимают один аргумент, могут случайно использоваться как неявные преобразования типов, даже если они не предназначены для этого.
- Когда конструктор с одним аргументом не помечен как explicit, нет надежного способа определить, предназначен ли он для определения неявного преобразования, или автор просто забыл это пометить.
- Неявные преобразования могут приводить к неоднозначностям места вызова, особенно когда есть двунаправленные неявные преобразования. Это может быть вызвано либо наличием двух типов, которые оба обеспечивают неявное преобразование, либо одним типом, который имеет как неявный конструктор, так и неявный оператор преобразования типа. Инициализация списка может страдать от тех же проблем, если целевой тип неявный, особенно если список содержит только один элемент.
Решение:
Операторы преобразования типов и конструкторы, вызываемые с одним аргументом, должны быть помечены как explicit в определении класса. В качестве исключения, конструкторы копирования и перемещения не должны быть explicit, поскольку они не выполняют преобразование типов.
Неявные преобразования иногда могут быть необходимы и уместны для типов, которые разработаны как взаимозаменяемые, например, когда объекты двух типов являются просто разными представлениями одного и того же базового значения. В этом случае свяжитесь с руководителями вашего проекта, чтобы запросить отказ от этого правила.
Конструкторы, которые не могут быть вызваны с одним аргументом, могут опускать explicit. Конструкторы, которые принимают один параметр std::initializer_list, также должны опускать explicit, чтобы поддерживать инициализацию копированием (например, MyType m = {1, 2};).
Правила оформления переменных
Переменных в классах и структурах может быть много, часто перегруженные объёмом переменных, классы становятся трудно-читаемыми или вовсе не понятными новым разработчикам. Новые разработчики могут тратить много часов, а то и дней, чтобы понять суть класса или структуры, а также замысел автора написавшего код.
Пример трудночитаемого кода:
class ConsumerRecord
{
public:
// ConsumerRecord will take the ownership of msg (rd_kafka_message_t*)
explicit ConsumerRecord(rd_kafka_message_t* msg): _rk_msg(msg, rd_kafka_message_destroy) {}
/**
* The topic this record is received from.
*/
Topic topic() const { return _rk_msg->rkt ? rd_kafka_topic_name(_rk_msg->rkt): ""; }
/**
* The partition from which this record is received.
*/
Partition partition() const { return _rk_msg->partition; }
/**
* The position of this record in the corresponding Kafka partition.
*/
Offset offset() const { return _rk_msg->offset; }
/**
* The key (or null if no key is specified).
*/
Key key() const { return Key(_rk_msg->key, _rk_msg->key_len); }
/**
* The value.
*/
Value value() const { return Value(_rk_msg->payload, _rk_msg->len); }
/**
* The timestamp of the record.
*/
Timestamp timestamp() const
{
rd_kafka_timestamp_type_t tstype{};
const Timestamp::Value tsValue = rd_kafka_message_timestamp(_rk_msg.get(), &tstype);
return {tsValue, tstype};
}
/**
* The headers of the record.
*/
Headers headers() const;
/**
* Return just one (the very last) header's value for the given key.
*/
Header::Value lastHeaderValue(const Header::Key& key);
/**
* The error.
*
* Possible cases:
* 1. Success
* - RD_KAFKA_RESP_ERR_NO_ERROR (0), -- got a message successfully
* - RD_KAFKA_RESP_ERR__PARTITION_EOF, -- reached the end of a partition (got no message)
* 2. Failure
* - [Error Codes] (https://cwiki.apache.org/confluence/display/KAFKA/A+Guide+To+The+Kafka+Protocol#AGuideToTheKafkaProtocol-ErrorCodes)
*/
Error error() const { return Error{_rk_msg->err}; }
/**
* Obtains explanatory string.
*/
std::string toString() const;
private:
using rd_kafka_message_shared_ptr = std::shared_ptr<rd_kafka_message_t>;
rd_kafka_message_shared_ptr _rk_msg;
};
В самом начале идёт объявление конструктора, в котором производится инициализация внутренней приватной переменной _rk_msg, ну а где она находится, это ещё нужно найти, в примере класс не такого большого размера, но уже есть трудности с читаемостью, что будет когда класс вырастет в 10 раз?
Решение:
Создавайте переменные простые, короткие и семантически-понятные, располагайте переменные в порядке возрастания по длине названия, от меньшего к большему в виде пирамиды, так глазу человека будет проще найти и увидеть нужную переменную в коде и будет проще визуализировать код в голове, не видя его на экране монитора. Располагайте всегда, переменные в классе и структуре на самой верхней строке, далее должны располагаться STL контейнеры, после которых должны идти указатели, потом объявления приватных методов, после которых идут объявления публичных методов, следом следуют операторы, на самой нижней позиции должны располагаться конструкторы перемещения, конструкторы копирования, список основных конструкторов и деструктор.
Пример:
/**
* URL Класс URL-адреса
*/
typedef class URL {
public:
// Порт сервера
uint32_t port;
public:
// Тип протокола интернета AF_INET или AF_INET6
int32_t family;
public:
std::string ip; // IP-адрес сервера
std::string host; // Хост сервера
public:
std::string user; // Пользователь
std::string pass; // Пароль
public:
std::string domain; // Доменное имя
std::string schema; // Протокол передачи данных
std::string anchor; // Якорь URL-запроса
public:
// Путь URL-запроса
std::vector <std::string> path;
// Параметры URL-запроса
std::vector <std::pair <std::string, std::string>> params;
public:
// Функция выполняемая при генерации URL адреса
std::function <std::string (const URL *, const URI *)> callback;
public:
/**
* clear Метод очистки
*/
void clear();
public:
/**
* empty Метод проверки на существование данных
* @return результат проверки
*/
bool empty() const;
public:
/**
* Оператор [=] перемещения параметров URL-адреса
* @param url объект URL-адреса для получения параметров
* @return параметры URL-адреса
*/
URL & operator = (URL && url);
/**
* Оператор [=] присванивания параметров URL-адреса
* @param url объект URL-адреса для получения параметров
* @return параметры URL-адреса
*/
URL & operator = (const URL & url);
public:
/**
* Оператор сравнения
* @param url параметры URL-адреса
* @return результат сравнения
*/
bool operator == (const URL & url);
public:
/**
* URL Конструктор перемещения
* @param url параметры URL-адреса
*/
URL(URL && url);
/**
* URL Конструктор копирования
* @param url параметры URL-адреса
*/
URL(const URL & url);
public:
/**
* URL Конструктор
*/
URL(){}
/**
* ~URL Деструктор
*/
~URL(){}
} url_t;
Семантика классов и структур
Разбиение информации на группы помогает лучше запомнить её. Группирование небольших фрагментов в единое целое усиливает естественную тенденцию мозга запоминать большие фрагменты лучше, чем сами фрагменты.
Несколько советов, как разбивать информацию на группы:
- Искать связи. Можно сгруппировать элементы, например, потому что каждый из них пишется четырьмя буквами, потому что они начинаются с одной и той же буквы или потому, что у них схожее назначение.
- Создавать ассоциации. Связывание групп предметов с вещами из памяти также может помочь сделать их более запоминающимися. Например, можно ассоциировать яйца, пищевую соду и шоколадную стружку с вкусным печеньем, которое пекла мама.
- Использовать эффект Ресторфф. Суть метода в том, чтобы в большом объёме информации разбивать её на группы, которые не похожи друг на друга, и запоминать их по принципу отличия. Яркие объекты среди однородных всегда привлекают внимание и лучше откладываются в памяти.
Способы, которыми индивидуум группирует информационный массив, в значительной степени имеют субъективный характер и зависят от особенностей восприятия и опыта индивидуума.
Из всего вышеизложенного мы понимаем, что:
- Код нужно сегментировать на семантические группы - это поможет его не только хорошо запоминать, но и быстро читать и хорошо ориентироваться.
- Каждый блок кода, должен быть семантически-понятным и самодокументируемым.
- Разбитые на сегменты код, должен состоять из небольших блоков и быть простым и понятным.
Решение:
Мы предлагаем разбивать на сегменты кода в классах и структурах содержимое, по смыслу их действия а также по их названиям. Например, если в структуре или классе, встречаются переменные: host, port и error то логично сгруппировать переменные host и port вместе, а переменную error вынести в отедльный сегмент. Разделять блоки для семантически-понятных сегментов, мы предлагаем с помощью модификаторов public, private и protected. Пример:
/**
* Server Класс параметров сервера
*/
typedef class Server {
public:
// Порт сервера
uint32_t port;
// Хост сервера
std::string host;
public:
// Ошибка состояния сервера
std::string error;
public:
/**
* Server Конструктор
*/
Server() : port(8080), host{"127.0.0.1"}, error{"None"} {}
/**
* ~Server Деструктор
*/
~Server(){}
} server_t;
Копируемые и перемещаемые типы
Открытый API класса должен четко указывать, является ли класс копируемым, только перемещаемым или ни копируемым, ни перемещаемым. Поддерживайте копирование и/или перемещение, если эти операции понятны и значимы для вашего типа.
Определение:
Перемещаемый тип — это тот, который может быть инициализирован и назначен из временных объектов.
Копируемый тип — это тот, который может быть инициализирован или назначен из любого другого объекта того же типа (поэтому он также является перемещаемым по определению), с условием, что значение источника не изменится. std::unique_ptr <int32_t> — это пример перемещаемого, но не копируемого типа (поскольку значение источника std::unique_ptr <int32_t> должно быть изменено во время назначения). int и std::string — это примеры перемещаемых типов, которые также являются копируемыми. (Для int операции перемещения и копирования одинаковы; для std::string существует операция перемещения, которая менее затратна, чем копирование.)
Для пользовательских типов поведение копирования определяется конструктором копирования и оператором копирования-присваивания. Поведение перемещения определяется конструктором перемещения и оператором перемещения-присваивания, если они существуют, или конструктором копирования и оператором копирования-присваивания в противном случае.
Конструкторы копирования/перемещения могут неявно вызываться компилятором в некоторых ситуациях, например, при передаче объектов по значению.
Плюсы:
Объекты копируемых и перемещаемых типов могут передаваться и возвращаться по значению, что делает API проще, безопаснее и более общими. В отличие от передачи объектов по указателю или ссылке, нет риска путаницы с владением, временем жизни, изменчивостью и подобными проблемами, и нет необходимости указывать их в контракте. Это также предотвращает нелокальные взаимодействия между клиентом и реализацией, что упрощает их понимание, поддержку и оптимизацию компилятором. Кроме того, такие объекты можно использовать с универсальными API, требующими передачи по значению, такими как большинство контейнеров, и они обеспечивают дополнительную гибкость, например, в композиции типов.
Конструкторы копирования/перемещения и операторы присваивания обычно легче определить правильно, чем альтернативы, такие как Clone(), CopyFrom() или Swap(), поскольку они могут быть сгенерированы компилятором либо неявно, либо с = default. Они лаконичны и гарантируют, что все члены данных будут скопированы. Конструкторы копирования и перемещения также, как правило, более эффективны, поскольку они не требуют выделения кучи или отдельных шагов инициализации и назначения, и они подходят для оптимизаций, таких как исключение копирования.
Операции перемещения позволяют неявно и эффективно переносить ресурсы из объектов rvalue. Это позволяет использовать более простой стиль кодирования в некоторых случаях.
Минусы:
Некоторые типы не обязательно должны быть копируемыми, и предоставление операций копирования для таких типов может быть запутанным, бессмысленным или совершенно неверным. Типы, представляющие одиночные объекты (Registerer), объекты, привязанные к определенной области действия (Cleanup) или тесно связанные с идентичностью объекта (Mutex), не могут быть скопированы осмысленно. Операции копирования для типов базового класса, которые должны использоваться полиморфно, опасны, поскольку их использование может привести к нарезке объекта. Операции копирования по умолчанию или небрежно реализованные могут быть неверными, а возникающие ошибки могут быть запутанными и сложными для диагностики.
Конструкторы копирования вызываются неявно, из-за чего вызов легко пропустить. Это может вызвать путаницу у программистов, привыкших к языкам, где передача по ссылке является общепринятой или обязательной. Это также может способствовать чрезмерному копированию, что может вызвать проблемы с производительностью.
Решение:
Открытый интерфейс каждого класса должен четко указывать, какие операции копирования и перемещения поддерживает класс. Обычно это должно принимать форму явного объявления и/или удаления соответствующих операций в открытом разделе объявления.
В частности, копируемый класс должен явно объявлять операции копирования, класс только для перемещения должен явно объявлять операции перемещения, а некопируемый/перемещаемый класс должен явно удалять операции копирования. Копируемый класс также может объявлять операции перемещения для поддержки эффективных перемещений. Явное объявление или удаление всех четырёх операций копирования/перемещения разрешено, но не обязательно. Если вы предоставляете оператор присваивания копирования или перемещения, вы также должны предоставить соответствующий конструктор.
class Copyable {
public:
Copyable(const Copyable & other) = default;
Copyable & operator = (const Copyable & other) = default;
// Неявные операции перемещения подавляются приведенными выше объявлениями.
// Вы можете явно объявить операции перемещения для поддержки эффективных перемещений.
};
class MoveOnly {
public:
MoveOnly(MoveOnly && other) = default;
MoveOnly & operator = (MoveOnly && other) = default;
// Операции копирования неявно удаляются, но вы можете
// Если хотите, скажите это прямо:
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;
};
Эти объявления/удаления можно опустить только в том случае, если они очевидны:
- Если класс не имеет закрытого раздела, как структура или базовый класс только для интерфейса, то копируемость/перемещаемость может быть определена копируемостью/перемещаемостью любых открытых членов данных.
- Если базовый класс явно не копируем и не перемещаем, производные классы, естественно, не будут ни копируемыми, ни перемещаемыми. Базовый класс только для интерфейса, который оставляет эти операции неявными, недостаточен для того, чтобы сделать конкретные подклассы понятными.
- Обратите внимание, что если вы явно объявляете или удаляете либо конструктор, либо операцию присваивания для копирования, другая операция копирования неочевидна и должна быть объявлена или удалена. То же самое касается операций перемещения.
Тип не должен быть копируемым/перемещаемым, если значение копирования/перемещения непонятно случайному пользователю или если это влечет за собой непредвиденные расходы. Операции перемещения для копируемых типов являются строго оптимизацией производительности и являются потенциальным источником ошибок и сложности, поэтому избегайте их определения, если они не являются значительно более эффективными, чем соответствующие операции копирования. Если ваш тип предоставляет операции копирования, рекомендуется спроектировать ваш класс так, чтобы реализация этих операций по умолчанию была правильной. Не забудьте проверить правильность любых операций по умолчанию, как и любой другой код.
Чтобы исключить риск срезов, предпочитайте делать базовые классы абстрактными, делая их конструкторы защищенными, объявляя их деструкторы защищенными или предоставляя им одну или несколько чисто виртуальных функций-членов. Предпочитайте избегать вывода из конкретных классов.
Что лучше, структуры или классы?
Используйте структуру только для пассивных объектов, которые несут данные; все остальное является классом.
Ключевые слова struct и class ведут себя почти одинаково в C++. Мы добавляем наши собственные семантические значения к каждому ключевому слову, поэтому вам следует использовать соответствующее ключевое слово для типа данных, который вы определяете.
структуры должны использоваться для пассивных объектов, которые несут данные, и могут иметь связанные константы. Все поля должны быть открытыми. Структура не должна иметь инвариантов, которые подразумевают отношения между различными полями, поскольку прямой доступ пользователя к этим полям может нарушить эти инварианты. Конструкторы, деструкторы и вспомогательные методы могут присутствовать; однако эти методы не должны требовать или обеспечивать какие-либо инварианты.
Если требуется больше функциональности или инвариантов или структура имеет широкую видимость и, как ожидается, будет развиваться, то класс будет более подходящим. Если вы сомневаетесь, сделайте его классом.
Для согласованности с STL вы можете использовать struct вместо class для типов без состояния, таких как traits, метафункции шаблонов и некоторые функторы.
Обратите внимание, что переменные-члены в структурах и классах имеют разные правила именования.
Что лучше, структуры или пары и кортежи?
Предпочитайте использовать структуру вместо пары или кортежа, когда элементы могут иметь осмысленные имена.
Хотя использование пар и кортежей может избежать необходимости определять пользовательский тип, потенциально экономя работу при написании кода, осмысленное имя поля почти всегда будет намного понятнее при чтении кода, чем .first, .second или std::get . Хотя введение в C++14 std::get для доступа к элементу кортежа по типу, а не по индексу (когда тип уникален) иногда может частично смягчить это, имя поля обычно существенно понятнее и информативнее, чем тип.
Пары и кортежи могут быть уместны в универсальном коде, где нет конкретных значений для элементов пары или кортежа. Их использование также может потребоваться для взаимодействия с существующим кодом или API.
Наследование
Композиция часто более уместна, чем наследование. При использовании наследования сделайте его публичным.
Определение:
Когда подкласс наследует от базового класса, он включает определения всех данных и операций, которые определяет базовый класс. «Наследование интерфейса» — это наследование от чистого абстрактного базового класса (без состояния или определенных методов); все остальное наследование — это «наследование реализации».
Плюсы:
Наследование реализации уменьшает размер кода за счет повторного использования кода базового класса, поскольку оно специализируется на существующем типе. Поскольку наследование является объявлением времени компиляции, вы и компилятор можете понять операцию и обнаружить ошибки. Наследование интерфейса может использоваться для программного обеспечения того, чтобы класс предоставлял определенный API. Опять же, компилятор может обнаружить ошибки, в данном случае, когда класс не определяет необходимый метод API.
Минусы:
Для наследования реализации, поскольку код, реализующий подкласс, распределен между базой и подклассом, может быть сложнее понять реализацию. Подкласс не может переопределять функции, которые не являются виртуальными, поэтому подкласс не может изменить реализацию.
Множественное наследование особенно проблематично, поскольку оно часто налагает более высокие издержки производительности (фактически, падение производительности от одиночного наследования к множественному наследованию часто может быть больше, чем падение производительности от обычной к виртуальной диспетчеризации), и поскольку оно рискует привести к «ромбовидным» шаблонам наследования, которые склонны к неоднозначности, путанице и явным ошибкам.
Решение:
Все наследование должно быть публичным. Если вы хотите сделать частное наследование, вам следует включить экземпляр базового класса в качестве члена. Вы можете использовать final для классов, когда вы не собираетесь поддерживать их использование в качестве базовых классов.
Не злоупотребляйте наследованием реализации. Композиция часто более уместна. Попробуйте ограничить использование наследования случаем «is-a»: Bar подклассы Foo, если можно обоснованно сказать, что Bar «является видом» Foo.
Ограничьте использование protected теми функциями-членами, к которым может потребоваться доступ из подклассов. Обратите внимание, что члены данных должны быть закрытыми.
Явно аннотируйте переопределения виртуальных функций или виртуальных деструкторов только одним спецификатором override или (реже) final. Не используйте virtual при объявлении переопределения. Обоснование: функция или деструктор, помеченные override или final, которые не являются переопределением виртуальной функции базового класса, не будут компилироваться, и это помогает выявлять распространенные ошибки. Спецификаторы служат в качестве документации; если спецификатор отсутствует, читателю необходимо проверить всех предков рассматриваемого класса, чтобы определить, является ли функция или деструктор виртуальным или нет.
Множественное наследование разрешено, но множественное наследование реализации настоятельно не рекомендуется.
Перегрузка оператора
Перегружайте операторы разумно. Не используйте определяемые пользователем литералы.
Определение:
C++ позволяет пользовательскому коду объявлять перегруженные версии встроенных операторов с помощью ключевого слова operator, пока один из параметров является типом, определенным пользователем. Ключевое слово operator также позволяет пользовательскому коду определять новые виды литералов с помощью operator "", и определять функции преобразования типов, такие как operator bool().
Плюсы:
Перегрузка операторов может сделать код более лаконичным и интуитивно понятным, позволяя пользовательским типам вести себя так же, как встроенные типы. Перегруженные операторы — это идиоматические названия определенных операций (например, ==, <, = и <<), и соблюдение этих соглашений может сделать пользовательские типы более читабельными и позволить им взаимодействовать с библиотеками, которые ожидают эти названия.
Пользовательские литералы — это очень лаконичная нотация для создания объектов пользовательских типов.
Минусы:
- Предоставление правильного, последовательного и не вызывающего удивления набора перегрузок операторов требует некоторой осторожности, и невыполнение этого требования может привести к путанице и ошибкам.
- Чрезмерное использование операторов может привести к запутанному коду, особенно если семантика перегруженного оператора не соответствует соглашению.
- Опасности перегрузки функций применимы к перегрузке операторов в той же степени, если не в большей.
- Перегрузки операторов могут обмануть нашу интуицию, заставив думать, что дорогостоящие операции — это дешевые встроенные операции.
- Для поиска мест вызова перегруженных операторов может потребоваться поисковый инструмент, который знает синтаксис C++, а не, например, grep.
- Если вы неправильно указали тип аргумента перегруженного оператора, вы можете получить другую перегрузку, а не ошибку компилятора. Например, foo < bar может делать одно, а &foo < &bar — совсем другое.
- Некоторые перегрузки операторов по своей сути опасны. Перегрузка унарного & может привести к тому, что один и тот же код будет иметь разные значения в зависимости от того, видно ли объявление перегрузки. Перегрузки &&, || и , (запятая) не могут соответствовать семантике порядка вычисления встроенных операторов.
- Операторы часто определяются вне класса, поэтому существует риск того, что разные файлы введут разные определения одного и того же оператора. Если оба определения связаны в один и тот же двоичный файл, это приведет к неопределенному поведению, которое может проявиться в виде тонких ошибок во время выполнения.
- Пользовательские литералы (UDL) позволяют создавать новые синтаксические формы, которые незнакомы даже опытным программистам на C++, такие как "Hello World"sv как сокращение для std::string_view(“Hello World”). Существующие нотации более понятны, хотя и менее лаконичны.
- Поскольку они не могут быть квалифицированы пространством имён, использование UDL также требует использования директив using (которые мы запрещаем) или объявлений using (которые мы запрещаем в заголовочных файлах, за исключением случаев, когда импортированные имена являются частью интерфейса, предоставляемого рассматриваемым заголовочным файлом). Учитывая, что в заголовочных файлах не должно быть суффиксов UDL, мы предпочитаем избегать различий в соглашениях для литералов между заголовочными файлами и исходными файлами.
Решение:
Определяйте перегруженные операторы только в том случае, если их значение очевидно, не вызывает удивления и согласуется с соответствующими встроенными операторами. Например, используйте | как побитовое или логическое ИЛИ, а не как конвейер в стиле оболочки.
Определяйте операторы только для своих собственных типов. Точнее, определяйте их в тех же заголовках, файлах .cpp и пространствах имён, что и типы, с которыми они работают. Таким образом, операторы будут доступны везде, где находится тип, что сведет к минимуму риск множественных определений. Если возможно, избегайте определения операторов как шаблонов, поскольку они должны удовлетворять этому правилу для любых возможных аргументов шаблона. Если вы определяете оператор, также определите любые связанные операторы, которые имеют смысл, и убедитесь, что они определены согласованно.
Предпочитайте определять немодифицирующие бинарные операторы как функции, не являющиеся членами. Если бинарный оператор определён как член класса, неявные преобразования будут применяться к правому аргументу, но не к левому. Это запутает ваших пользователей, если a + b скомпилируется, а b + a — нет.
Для типа T, значения которого можно сравнивать на равенство, определите не являющийся членом оператор == и задокументируйте, когда два значения типа T считаются равными. Если есть одно очевидное понятие того, когда значение t1 типа T меньше другого такого значения t2, то вы также можете определить оператор <=>, который должен соответствовать оператору ==. Предпочитайте не перегружать другие операторы сравнения и упорядочивания.
Не старайтесь избегать определения перегрузок операторов. Например, предпочитайте определять ==, = и <<, а не Equals(), CopyFrom() и PrintTo(). И наоборот, не определяйте перегрузки операторов только потому, что другие библиотеки их ожидают. Например, если у вашего типа нет естественного порядка, но вы хотите сохранить его в std::set, используйте пользовательский компаратор вместо перегрузки <.
Не перегружайте &&, ||, , (запятая) или унарный &. Не перегружайте оператор "", т.е. не вводите определяемые пользователем литералы. Не используйте такие литералы, предоставленные другими (включая стандартную библиотеку).
Операторы преобразования типов рассматриваются в разделе о неявных преобразованиях. Оператор = рассматривается в разделе о конструкторах копирования. Перегрузка << для использования с потоками рассматривается в разделе о потоках. См. также правила перегрузки функций, которые применяются и к перегрузке операторов.
Контроль доступа
Сделайте элементы данных классов закрытыми, если только они не являются константами. Это упрощает рассуждения об инвариантах, за счёт некоторого простого шаблона в виде аксессоров (обычно const), если это необходимо.
По техническим причинам мы разрешаем защищать элементы данных класса тестовой фикстуры, определенного в файле .cpp, при использовании Google Test. Если класс тестовой фикстуры определен вне файла .cpp, в котором он используется, например, в файле .hpp, сделайте элементы данных закрытыми.
Декларация Ордера
Группируйте похожие объявления вместе, размещая общедоступные части раньше.
Определение класса обычно должно начинаться с раздела private:, за которым следует protected:, затем public:. Пропускайте разделы, которые будут пустыми.
В каждом разделе предпочитайте группировать похожие виды объявлений вместе и предпочитайте следующий порядок:
- Типы и псевдонимы типов (typedef, using, enum, вложенные структуры и классы, а также дружественные типы)
- (Необязательно, только для структур) нестатические члены данных
- Статические константы
- Фабричные функции
- Конструкторы и операторы присваивания
- Деструктор
- Все остальные функции (статические и нестатические функции-члены, а также дружественные функции)
- Все остальные члены данных (статические и нестатические)
Не помещайте большие определения методов в строку определения класса. Обычно только тривиальные или критичные к производительности и очень короткие методы могут быть определены в строке. Подробнее см. в разделе Встроенные функции.
Функции
Входы и выходы
Выходные данные функции C++ естественным образом предоставляются через возвращаемое значение и иногда через выходные параметры (или параметры ввода/вывода).
Предпочитайте возвращаемые значения выходным параметрам: они улучшают читаемость и часто обеспечивают такую же или лучшую производительность.
Предпочитайте возвращать по значению или, если это невозможно, возвращать по ссылке. Избегайте возврата необработанного указателя, если он не может быть нулевым.
Параметры являются либо входными данными функции, либо выходными данными функции, либо и тем, и другим. Необязательные входные параметры обычно должны быть значениями или константными ссылками, в то время как необязательные выходные и входные/выходные параметры обычно должны быть ссылками (которые не могут быть нулевыми). Как правило, используйте std::optional для представления необязательных входных данных по значению и используйте константный указатель, когда необязательная форма использовала бы ссылку. Используйте неконстантные указатели для представления необязательных выходных данных и необязательных входных/выходных параметров.
Избегайте определения функций, которым требуется ссылочный параметр для того, чтобы пережить вызов. В некоторых случаях ссылочные параметры могут привязываться к временным объектам, что приводит к ошибкам на протяжении всего времени существования. Вместо этого найдите способ устранить требование к сроку службы (например, скопировав параметр) или передайте сохраненные параметры указателем и задокументируйте требования к сроку службы и ненулевым параметрам.
При упорядочивании параметров функции поместите все входные параметры перед любыми выходными параметрами. В частности, не добавляйте новые параметры в конец функции только потому, что они новые; поместите новые входные параметры перед выходными параметрами. Это не жесткое правило. Параметры, которые являются как входными, так и выходными, мутят воду, и, как всегда, согласованность со связанными функциями может потребовать от вас нарушить правило. Функции с переменным числом аргументов также могут потребовать необычного порядка параметров.
Создавайте короткие функции
Предпочитайте небольшие и целенаправленные функции.
Мы признаем, что длинные функции иногда уместны, поэтому на длину функций не накладывается жестких ограничений. Если функция превышает около 40 строк, подумайте, можно ли ее разбить, не навредив структуре программы.
Даже если ваша длинная функция сейчас работает идеально, кто-то, кто изменит ее через несколько месяцев, может добавить новое поведение. Это может привести к ошибкам, которые трудно найти. Сохранение ваших функций короткими и простыми облегчает другим людям чтение и изменение вашего кода. Маленькие функции также легче тестировать.
Вы можете обнаружить длинные и сложные функции при работе с некоторым кодом. Не бойтесь изменять существующий код: если работа с такой функцией окажется сложной, вы обнаружите, что ошибки трудно отлаживать, или вы хотите использовать ее часть в нескольких разных контекстах, рассмотрите возможность разбиения функции на более мелкие и более управляемые части.
Перегрузка функций
Используйте перегруженные функции (включая конструкторы) только в том случае, если читатель, глядя на место вызова, может получить хорошее представление о том, что происходит, без необходимости сначала выяснять, какая именно перегрузка вызывается.
Определение:
Вы можете написать функцию, которая принимает 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);
};
Плюсы:
Перегрузка может сделать код более интуитивным, позволяя функции с одинаковым именем принимать разные аргументы. Это может быть необходимо для шаблонизированного кода и может быть удобно для посетителей.
Перегрузка на основе квалификации const или ref может сделать служебный код более удобным, более эффективным или и тем, и другим.
Минусы:
Если функция перегружена только типами аргументов, читателю, возможно, придется понять сложные правила соответствия C++, чтобы понять, что происходит. Также многих смущает семантика наследования, если производный класс переопределяет только некоторые варианты функции.
Решение:
Вы можете перегрузить функцию, когда между вариантами нет семантических различий. Эти перегрузки могут различаться по типам, квалификаторам или количеству аргументов. Однако читателю такого вызова не нужно знать, какой член набора перегрузок выбран, а нужно знать только, что вызывается что-то из набора. Если вы можете задокументировать все записи в наборе перегрузок одним комментарием в заголовке, это хороший признак того, что это хорошо спроектированный набор перегрузок.
Аргументы функций
Аргументы функций должны сожержать модификатор const за исключением случаев, когда данные должны передаваться по не константной ссылке. В большом коде, названия входных параметров могут пересекаться с локальными переменными, чтобы исключить ошибку случайного переопределения входных параметров, модификатор const просто необходим. Например:
int32_t calc(int32_t a, int32_t b){
int32_t c = 0;
if(a > b)
// ПЛОХО: входной параметр "a" был переопределён в теле функции
a += (a - b);
else if(a < b)
// ПЛОХО: входной параметр "b" был переопределён в теле функции
b += (b - a);
c = (a + b);
return c;
}
int32_t calc(const int32_t a, const int32_t b){
int32_t c = 0;
if(a > b)
c += (a + (a - b));
else if(a < b)
c += (b + (b - a));
else if(a == b)
c = (a + b)
return c;
}
Аргументы по умолчанию
Аргументы по умолчанию разрешены для невиртуальных функций, когда гарантированно, что значение по умолчанию всегда будет иметь одно и то же значение. Следуйте тем же ограничениям, что и для перегрузки функций, и предпочитайте перегруженные функции, если читаемость, полученная с аргументами по умолчанию, не перевешивает недостатки, указанные ниже.
Плюсы:
Часто у вас есть функция, которая использует значения по умолчанию, но иногда вы хотите переопределить значения по умолчанию. Параметры по умолчанию позволяют легко сделать это без необходимости определять множество функций для редких исключений. По сравнению с перегрузкой функции аргументы по умолчанию имеют более чистый синтаксис, с меньшим количеством шаблонов и более четким различием между «обязательными» и «необязательными» аргументами.
Минусы:
Аргументы по умолчанию — это ещё один способ достижения семантики перегруженных функций, поэтому применимы все причины не перегружать функции.
Значения по умолчанию для аргументов в вызове виртуальной функции определяются статическим типом целевого объекта, и нет гарантии, что все переопределения данной функции объявляют те же значения по умолчанию.
Параметры по умолчанию повторно оцениваются на каждом месте вызова, что может раздуть сгенерированный код. Читатели также могут ожидать, что значение по умолчанию будет фиксированным при объявлении, а не меняться при каждом вызове.
Указатели функций сбивают с толку при наличии аргументов по умолчанию, поскольку сигнатура функции часто не совпадает с сигнатурой вызова. Добавление перегрузок функций позволяет избежать этих проблем.
Решение:
Аргументы по умолчанию запрещены в виртуальных функциях, где они не работают должным образом, и в случаях, когда указанное значение по умолчанию может не оцениваться в одно и то же значение в зависимости от того, когда оно было оценено. (Например, не пишите void f(int n = counter++);.)
В некоторых других случаях аргументы по умолчанию могут улучшить читаемость объявлений функций в достаточной степени, чтобы преодолеть недостатки, описанные выше, поэтому они разрешены. Если есть сомнения, используйте перегрузки.
Синтаксис типа возвращаемого значения
Используйте конечные типы возвращаемых данных только в тех случаях, когда использование обычного синтаксиса (начальные типы возвращаемых данных) непрактично или гораздо менее удобно для чтения.
Определение:
C++ допускает две различные формы деклараций функций. В старой форме возвращаемый тип указывается перед именем функции. Например:
int32_t foo(int32_t x);
Новая форма использует ключевое слово auto перед именем функции и завершающий тип возвращаемого значения после списка аргументов. Например, приведенное выше объявление может быть эквивалентно записано:
auto foo(int32_t x) -> int32_t;
Конечный возвращаемый тип находится в области действия функции. Это не имеет значения для простого случая, например int, но имеет значение для более сложных случаев, например, для типов, объявленных в области действия класса, или типов, записанных в терминах параметров функции.
Плюсы:
Конечные возвращаемые типы — единственный способ явно указать возвращаемый тип лямбда-выражения. В некоторых случаях компилятор может вывести возвращаемый тип лямбда-выражения, но не во всех случаях. Даже когда компилятор может вывести его автоматически, иногда явное указание будет понятнее для читателей.
Иногда проще и читабельнее указать возвращаемый тип после того, как список параметров функции уже появился. Это особенно верно, когда возвращаемый тип зависит от параметров шаблона. Например:
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u);
против
template <typename T, typename U>
decltype(declval <T &> () + declval <U &> ()) add(T t, U u);
Минусы:
Синтаксис типа конечного возвращаемого значения относительно новый и не имеет аналогов в языках типа C++, таких как C и Java, поэтому некоторым читателям он может показаться незнакомым.
Существующие кодовые базы содержат огромное количество объявлений функций, которые не будут изменены для использования нового синтаксиса, поэтому реалистичным выбором будет использование только старого синтаксиса или использование смеси этих двух. Использование одной версии лучше для единообразия стиля.
Решение:
В большинстве случаев продолжайте использовать старый стиль объявления функций, где возвращаемый тип идет перед именем функции. Используйте новую форму с завершающим возвращаемым типом только в тех случаях, когда это необходимо (например, лямбды) или когда, помещая тип после списка параметров функции, вы можете записать тип в гораздо более читаемом виде. Последний случай должен быть редким; в основном это проблема в довольно сложном шаблонном коде, что не рекомендуется в большинстве случаев.
Специфическая магия Google
Существуют различные приемы и утилиты, которые мы используем для повышения надежности кода C++, а также различные способы использования C++, которые могут отличаться от тех, что вы видите где-либо ещё.
Право собственности и умные указатели
Предпочитают иметь одного, фиксированного владельца для динамически выделяемых объектов. Предпочитают передавать право собственности с помощью умных указателей.
Определение:
«Владение» — это метод учёта для управления динамически выделяемой памятью (и другими ресурсами). Владелец динамически выделяемого объекта — это объект или функция, которые отвечают за обеспечение его удаления, когда он больше не нужен. Иногда владение может быть общим, и в этом случае последний владелец обычно несёт ответственность за его удаление. Даже если владение не является общим, его можно передавать из одного фрагмента кода в другой.
«Умные» указатели — это классы, которые действуют как указатели, например, путем перегрузки операторов * и ->. Некоторые типы умных указателей можно использовать для автоматизации учёта владения, чтобы гарантировать выполнение этих обязанностей. std::unique_ptr — это тип умного указателя, который выражает исключительное владение динамически выделяемым объектом; объект удаляется, когда std::unique_ptr выходит из области видимости. Его нельзя скопировать, но можно переместить для представления передачи владения. std::shared_ptr — это тип умного указателя, который выражает общее владение динамически выделяемым объектом. std::shared_ptr можно копировать; право собственности на объект распределяется между всеми копиями, и объект удаляется при уничтожении последнего std::shared_ptr.
Плюсы:
- Практически невозможно управлять динамически выделенной памятью без какой-либо логики владения.
- Передача права собственности на объект может быть дешевле, чем его копирование (если копирование вообще возможно).
- Передача права собственности может быть проще, чем «заимствование» указателя или ссылки, поскольку это снижает необходимость координировать жизненный цикл объекта между двумя пользователями.
- Умные указатели могут улучшить читаемость, делая логику владения явной, самодокументируемой и недвусмысленной.
- Умные указатели могут исключить ручной учет владения, упрощая код и исключая большие классы ошибок.
- Для константных объектов общее владение может быть простой и эффективной альтернативой глубокому копированию.
Минусы:
- Право собственности должно быть представлено и передано с помощью указателей (умных или простых). Семантика указателей сложнее семантики значений, особенно в API: вам нужно беспокоиться не только о праве собственности, но и о псевдонимах, времени жизни и изменчивости, среди прочих проблем.
- Затраты на производительность семантики значений часто переоцениваются, поэтому преимущества производительности передачи права собственности могут не оправдать затраты на читаемость и сложность.
- API, которые передают право собственности, заставляют своих клиентов использовать единую модель управления памятью.
- Код, использующий умные указатели, менее явно указывает, где происходит освобождение ресурсов.
- std::unique_ptr выражает передачу права собственности с помощью семантики перемещения, которая является относительно новой и может сбить с толку некоторых программистов.
- Совместное владение может быть заманчивой альтернативой тщательному проектированию права собственности, запутывая проектирование системы.
- Совместное владение требует явного учета во время выполнения, что может быть дорогостоящим.
- В некоторых случаях (например, циклические ссылки) объекты с общим владением могут никогда не удаляться.
Решение:
Если необходимо динамическое выделение, предпочитайте сохранить владение за кодом, который его выделил. Если другому коду нужен доступ к объекту, рассмотрите возможность передачи ему копии или передачи указателя или ссылки без передачи владения. Предпочитайте использовать std::unique_ptr, чтобы сделать передачу владения явной. Например:
std::unique_ptr <Foo> FooFactory();
void FooConsumer(std::unique_ptr <Foo> ptr);
Не проектируйте свой код для использования совместного владения без очень веской причины. Одна из таких причин — избегать дорогостоящих операций копирования, но вам следует делать это только в том случае, если выигрыш в производительности значителен, а базовый объект является неизменяемым (т.е. std::shared_ptr ). Если вы используете совместное владение, предпочитайте std::shared_ptr.
Никогда не используйте std::auto_ptr. Вместо этого используйте std::unique_ptr.
Другие возможности C++
Ссылки Rvalue
Используйте ссылки rvalue только в определенных особых случаях, перечисленных ниже.
Определение:
Ссылки Rvalue — это тип ссылок, которые могут связываться только с временными объектами. Синтаксис похож на традиционный синтаксис ссылок. Например, void f(std::string && s); объявляет функцию, аргументом которой является ссылка rvalue на std::string.
Когда токен ‘&&’ применяется к неквалифицированному аргументу шаблона в параметре функции, применяются специальные правила вывода аргумента шаблона. Такая ссылка называется пересылаемой ссылкой.
Плюсы:
- Определение конструктора перемещения (конструктора, принимающего ссылку rvalue на тип класса) позволяет перемещать значение вместо его копирования. Например, если v1 — это std::vector std::string, то auto v2(std::move(v1)) скорее всего приведет к простой манипуляции указателем вместо копирования большого объема данных. Во многих случаях это может привести к значительному повышению производительности.
- Ссылки Rvalue позволяют реализовывать типы, которые можно перемещать, но нельзя копировать, что может быть полезно для типов, у которых нет разумного определения копирования, но вы все равно можете захотеть передать их в качестве аргументов функции, поместить их в контейнеры и т.д.
- std::move необходимо для эффективного использования некоторых типов стандартной библиотеки, таких как std::unique_ptr.
- Пересылка ссылок, использующих ссылочный токен rvalue, позволяет написать универсальную обертку функции, которая пересылает свои аргументы другой функции и работает независимо от того, являются ли её аргументы временными объектами и/или константами. Это называется «идеальной пересылкой».
Минусы:
- Ссылки Rvalue пока ещё не получили широкого распространения. Такие правила, как свертывание ссылок и специальное правило вывода для пересылаемых ссылок, несколько неясны.
- Ссылки Rvalue часто используются неправильно. Использование ссылок rvalue нелогично в сигнатурах, где ожидается, что аргумент будет иметь допустимое указанное состояние после вызова функции или где не выполняется операция перемещения.
Решение:
Не используйте ссылки rvalue (или не применяйте квалификатор && к методам), за исключением следующих случаев:
- Вы можете использовать их для определения конструкторов перемещения и операторов присваивания перемещения (как описано в разделе Копируемые и перемещаемые типы).
- Вы можете использовать их для определения методов с квалификацией &&, которые логически «потребляют» * this, оставляя его в непригодном для использования или пустом состоянии. Обратите внимание, что это применимо только к квалификаторам методов (которые идут после закрывающей скобки сигнатуры функции); если вы хотите «потребить» обычный параметр функции, предпочтительнее передавать его по значению.
- Вы можете использовать пересылаемые ссылки в сочетании с std::forward для поддержки идеальной пересылки.
- Вы можете использовать их для определения пар перегрузок, например, одна принимает Foo &&, а другая принимает const Foo &. Обычно предпочтительным решением является просто передача по значению, но перегруженная пара функций иногда обеспечивает лучшую производительность, например, если функции иногда не потребляют входные данные. Как всегда: если вы пишете более сложный код ради производительности, убедитесь, что у вас есть доказательства того, что это действительно помогает.
- Если вы не используете шаблоны при написании функций, то достаточно использовать std::move для удобного перемещения, если используются шаблоны и тип данных изначально не известен, то правильно использовать std::forward, пример:
class Obj {
private:
std::string _field;
public:
template <class T>
Obj(T && x) : _field(std::forward <T> (x)) {} // Забежали вперёд и сделали правильно
// Ниже объясним, почему без явной функции forward нельзя
}
Шаблон std::forward используется только в шаблонах (в нешаблонном коде хватает std::move). Он требует, чтобы тип был явно указан (иначе не отличишь Obj(string &&) от Obj(string &)), и либо ничего не делает, либо разворачивается в std::move.
class Obj {
private:
std::string _field;
public:
Obj(const std::string & x) : _field(x) {}
Obj(std::string && x) : _field(std::move(x)) {} // std::move нужен!!
}
Friends
Мы разрешаем использовать дружественные классы и функции в разумных пределах.
friends обычно должны быть определены в том же файле, чтобы читателю не приходилось искать в другом файле использование закрытых членов класса. Обычно friends используются для того, чтобы класс FooBuilder был другом Foo, чтобы он мог правильно конструировать внутреннее состояние Foo, не раскрывая это состояние миру. В некоторых случаях может быть полезно сделать класс unittest другом класса, который он тестирует.
friends расширяют, но не нарушают границу инкапсуляции класса. В некоторых случаях это лучше, чем делать член публичным, когда вы хотите предоставить доступ к нему только одному другому классу. Однако большинство классов должны взаимодействовать с другими классами исключительно через свои публичные члены.
Исключения
Мы не используем исключения C++.
Плюсы:
- Исключения позволяют более высоким уровням приложения решать, как обрабатывать сбои «не может произойти» в глубоко вложенных функциях, без запутывающего и подверженного ошибкам учета кодов ошибок.
- Исключения используются большинством других современных языков. Использование их в C++ сделало бы его более согласованным с Python, Java и C++, с которыми знакомы другие.
- Некоторые сторонние библиотеки C++ используют исключения, и их внутреннее отключение затрудняет интеграцию с этими библиотеками.
- Исключения — единственный способ для конструктора потерпеть неудачу. Мы можем имитировать это с помощью функции-фабрики или метода Init(), но для этого требуется выделение кучи или новое «недопустимое» состояние соответственно.
- Исключения действительно удобны в тестовых фреймворках.
Минусы:
- Когда вы добавляете оператор throw в существующую функцию, вы должны проверить все её транзитивные вызывающие. Либо они должны сделать по крайней мере базовую гарантию безопасности исключений, либо они не должны перехватывать исключение и быть довольны завершением программы в результате. Например, если f() вызывает g(), вызывает h(), а h выдает исключение, которое перехватывает f, g должен быть осторожен, иначе он может не очиститься должным образом.
- В более общем смысле исключения затрудняют оценку потока управления программами с помощью просмотра кода: функции могут возвращаться в местах, которых вы не ожидаете. Это вызывает трудности с поддержкой и отладкой. Вы можете минимизировать эти затраты с помощью некоторых правил о том, как и где можно использовать исключения, но ценой большего, что разработчик должен знать и понимать.
- Безопасность исключений требует как RAII, так и различных методов кодирования. Требуется множество вспомогательных механизмов, чтобы упростить написание правильного кода, безопасного для исключений. Кроме того, чтобы не требовать от читателей понимания всего графа вызовов, код, безопасный для исключений, должен изолировать логику, которая записывает в постоянное состояние, в фазу «фиксации». Это будет иметь как преимущества, так и издержки (возможно, когда вам придется скрывать код, чтобы изолировать фиксацию). Разрешение исключений заставит нас всегда платить эти издержки, даже если они того не стоят.
- Включение исключений добавляет данные в каждый созданный двоичный файл, увеличивая время компиляции (вероятно, немного) и, возможно, увеличивая нагрузку на адресное пространство.
- Наличие исключений может побудить разработчиков выдавать их, когда они не подходят, или восстанавливаться после них, когда это небезопасно. Например, неверный ввод пользователя не должен приводить к выдаче исключений. Нам пришлось бы сделать руководство по стилю еще длиннее, чтобы задокументировать эти ограничения!
Решение:
На первый взгляд, преимущества использования исключений перевешивают затраты, особенно в новых проектах. Однако для существующего кода введение исключений имеет последствия для всего зависимого кода. Если исключения могут распространяться за пределы нового проекта, также становится проблематично интегрировать новый проект в существующий код без исключений. Поскольку большая часть существующего кода C++ не готова иметь дело с исключениями, сравнительно сложно принять новый код, который генерирует исключения.
Учитывая, что существующий код не является толерантным к исключениям, затраты на использование исключений несколько выше, чем затраты в новом проекте. Процесс преобразования будет медленным и подверженным ошибкам. Мы не считаем, что доступные альтернативы исключениям, такие как коды ошибок и утверждения, представляют собой значительную нагрузку.
Наш совет против использования исключений основан не на философских или моральных основаниях, а на практических. Поскольку мы хотели бы использовать наши проекты с открытым исходным кодом, а это трудно сделать, если эти проекты используют исключения, нам также необходимо рекомендовать не использовать исключения в проектах с открытым исходным кодом. Все, вероятно, было бы иначе, если бы нам пришлось делать все заново с нуля.
Этот запрет также распространяется на функции, связанные с обработкой исключений, такие как std::exception_ptr и std::nested_exception.
Для кода Windows есть исключение из этого правила (без каламбура).
noexcept
Указывайте noexcept, за исключением случаев, когда это полезно и правильно.
Определение:
Спецификатор noexcept используется для указания того, будет ли функция выдавать исключения или нет. Если исключение выходит из функции, помеченной как noexcept, программа аварийно завершается через std::terminate.
Оператор noexcept выполняет проверку во время компиляции, которая возвращает true, если выражение объявлено не выдающим никаких исключений.
Плюсы:
- Указание конструкторов перемещения как noexcept повышает производительность в некоторых случаях, например, std::vector ::resize() перемещает, а не копирует объекты, если конструктор перемещения T — noexcept.
- Указание noexcept для функции может запустить оптимизацию компилятора в средах, где включены исключения, например, компилятору не нужно генерировать дополнительный код для раскручивания стека, если он знает, что исключения не могут быть выданы из-за спецификатора noexcept.
Минусы:
- В проектах, следующих этому руководству, в которых отключены исключения, сложно гарантировать, что спецификаторы noexcept корректны, и сложно определить, что вообще означает корректность.
- Трудно, если не невозможно, отменить noexcept, поскольку это устраняет гарантию, на которую могут полагаться вызывающие, способами, которые трудно обнаружить.
Решение:
Вы можете использовать noexcept, когда это полезно для производительности, если это точно отражает предполагаемую семантику вашей функции, т.е. если исключение каким-либо образом выбрасывается из тела функции, то оно представляет собой фатальную ошибку. Вы можете предположить, что noexcept в конструкторах перемещения имеет существенное преимущество в производительности. Если вы считаете, что указание noexcept в какой-то другой функции дает существенное преимущество в производительности, обсудите это с руководителями вашего проекта.
Предпочитайте безусловное noexcept, если исключения полностью отключены. В противном случае используйте условные спецификаторы noexcept с простыми условиями, способами, которые оценивают false только в тех немногих случаях, когда функция потенциально может выбросить исключение. Тесты могут включать проверку признаков типа на то, может ли задействованная операция выбросить исключение (например, std::is_nothrow_move_constructible для объектов, конструирующих перемещение), или на то, может ли выделение выбросить исключение (например, absl::default_allocator_is_nothrow для стандартного выделения по умолчанию). Обратите внимание, что во многих случаях единственной возможной причиной исключения является сбой выделения (мы считаем, что конструкторы перемещения не должны выдавать исключение, за исключением случаев сбоя выделения), и существует множество приложений, в которых целесообразно рассматривать исчерпание памяти как фатальную ошибку, а не как исключительное состояние, из которого ваша программа должна попытаться восстановиться. Даже для других потенциальных сбоев вы должны отдать приоритет простоте интерфейса, а не поддержке всех возможных сценариев выдачи исключений: вместо того, чтобы писать сложное предложение noexcept, которое зависит от того, может ли хэш-функция выдать исключение, например, просто задокументируйте, что ваш компонент не поддерживает выдачу хэш-функций, и сделайте его безусловным noexcept.
Информация о типе времени выполнения (RTTI)
Избегайте использования информации о типе времени выполнения (RTTI).
Определение:
RTTI позволяет программисту запрашивать класс C++ объекта во время выполнения. Это делается с помощью 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 затрудняет поддержку кода. Это может привести к деревьям решений на основе типов или операторам switch, разбросанным по всему коду, все из которых необходимо проверять при внесении дальнейших изменений.
Решение:
RTTI имеет законное применение, но подвержено злоупотреблениям, поэтому вы должны быть осторожны при его использовании. Вы можете свободно использовать его в unittests, но избегайте его, когда это возможно, в другом коде. В частности, подумайте дважды, прежде чем использовать 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 (double_value), или инициализацию фигурными скобками для преобразования арифметических типов, например int64_t y = int64_t{1} << 42. Не используйте форматы приведения, например (int) x, если только приведение не должно быть void. Вы можете использовать форматы приведения, например T(x), только если T является типом класса.
Определение:
В C++ введена отличная от C система приведения типов, которая различает типы операций приведения типов.
Плюсы:
Проблема с приведениями C заключается в неоднозначности операции; иногда вы делаете преобразование (например, (int) 3.5), а иногда вы делаете приведение (например, (int) “hello”). Инициализация фигурных скобок и приведения C++ часто могут помочь избежать этой неоднозначности. Кроме того, приведения C++ более заметны при поиске.
Минусы:
Синтаксис приведения типов в стиле C++ многосимвольный и громоздокий.
Решение:
В общем случае не используйте приведения в стиле C. Вместо этого используйте эти приведения в стиле C++, когда необходимо явное преобразование типов.
- Используйте инициализацию фигурными скобками для преобразования арифметических типов (например, int64_t). Это самый безопасный подход, поскольку код не будет компилироваться, если преобразование может привести к потере информации. Синтаксис также лаконичен.
- Используйте absl::implicit_cast для безопасного приведения иерархии типов вверх, например, приведения Foo * к SuperclassOfFoo * или приведения Foo * к const Foo *. C++ обычно делает это автоматически, но в некоторых ситуациях требуется явное приведение вверх, например, использование оператора ?:.
- Используйте static_cast как эквивалент приведения в стиле C, которое выполняет преобразование значений, когда вам нужно явно привести указатель из класса к его суперклассу или когда вам нужно явно привести указатель из суперкласса к подклассу. В этом последнем случае вы должны быть уверены, что ваш объект на самом деле является экземпляром подкласса.
- Используйте const_cast для удаления квалификатора const (см. const).
- Используйте reinterpret_cast для небезопасных преобразований типов указателей в целочисленные и другие типы указателей, включая void *, и обратно. Используйте это только в том случае, если вы знаете, что делаете, и понимаете проблемы с псевдонимами. Также рассмотрите возможность разыменования указателя (без приведения) и использования std::bit_cast для приведения полученного значения.
- Используйте std::bit_cast для интерпретации необработанных битов значения с использованием другого типа того же размера (каламбур типа), например, интерпретации битов double как int64_t.
Инструкции по использованию dynamic_cast см. в разделе RTTI.
Потоки
Используйте потоки там, где это уместно, и придерживайтесь «простых» вариантов использования. Перегружайте << для потоковой передачи только для типов, представляющих значения, и записывайте только видимое пользователю значение, а не подробности реализации.
Определение:
Потоки — это стандартная абстракция ввода-вывода в C++, примером которой является стандартный заголовок . Они широко используются в коде, в основном для отладочного ведения журнала и тестовой диагностики.
Плюсы:
Операторы потока << и >> предоставляют API для форматированного ввода-вывода, который легко изучить, переносить, повторно использовать и расширять. printf, напротив, даже не поддерживает std::string, не говоря уже о пользовательских типах, и его очень сложно использовать переносимо. printf также обязывает вас выбирать среди многочисленных немного отличающихся версий этой функции и ориентироваться в десятках спецификаторов преобразования.
Потоки предоставляют первоклассную поддержку консольного ввода-вывода через std::cin, std::cout, std::cerr и std::clog. API C тоже делают то же самое, но им мешает необходимость вручную буферизировать ввод.
Минусы:
- Форматирование потока можно настроить, изменяя состояние потока. Такие изменения постоянны, поэтому на поведение вашего кода может влиять вся предыдущая история потока, если только вы не постараетесь восстановить его до известного состояния каждый раз, когда другой код мог к нему прикоснуться. Пользовательский код может не только изменять встроенное состояние, но и добавлять новые переменные состояния и поведения через систему регистрации.
- Точно контролировать вывод потока сложно из-за вышеуказанных проблем, способа смешивания кода и данных в потоковом коде и использования перегрузки операторов (которая может выбрать перегрузку, отличную от ожидаемой).
- Практика построения вывода с помощью цепочек операторов << мешает интернационализации, поскольку она запекает порядок слов в коде, а поддержка локализации потоками несовершенна.
- API потоков тонкий и сложный, поэтому программисты должны приобретать опыт работы с ним, чтобы эффективно его использовать.
- Разрешение множества перегрузок << чрезвычайно затратно для компилятора. При повсеместном использовании в большой кодовой базе он может занять до 20% времени синтаксического и семантического анализа.
Решение:
Используйте потоки только тогда, когда они являются лучшим инструментом для работы. Обычно это происходит, когда ввод-вывод является специальным, локальным, понятным человеку и нацелен на других разработчиков, а не на конечных пользователей. Будьте последовательны с кодом вокруг вас и с кодовой базой в целом; если есть установленный инструмент для вашей проблемы, используйте его вместо этого. В частности, библиотеки журналирования обычно являются лучшим выбором, чем std::cerr или std::clog для диагностического вывода, а библиотеки в absl/strings или эквивалентные им обычно являются лучшим выбором, чем std::stringstream.
Избегайте использования потоков для ввода-вывода, который сталкивается с внешними пользователями или обрабатывает ненадежные данные. Вместо этого найдите и используйте соответствующие библиотеки шаблонов для решения таких проблем, как интернационализация, локализация и усиление безопасности.
Если вы используете потоки, избегайте частей API потоков с сохранением состояния (кроме состояния ошибки), таких как imbue(), xalloc() и register_callback(). Используйте явные функции форматирования (например, absl::StreamFormat()) вместо потоковых манипуляторов или флагов форматирования для управления деталями форматирования, такими как основание числа, точность или заполнение.
Перегружайте << как потоковый оператор для вашего типа, только если ваш тип представляет значение, а << записывает понятное человеку строковое представление этого значения. Избегайте раскрытия деталей реализации в выводе <<; если вам нужно вывести внутренние данные объекта для отладки, используйте вместо этого именованные функции (метод с именем DebugString() является наиболее распространенным соглашением).
Предварительный инкремент и предварительный декремент
Используйте префиксную форму (++i) операторов инкремента и декремента, если вам не нужна постфиксная семантика.
Определение:
Когда переменная увеличивается (++i или i++) или уменьшается (–i или i–), а значение выражения не используется, необходимо решить, следует ли выполнять преинкремент (декремент) или постинкремент (декремент).
Плюсы:
Постфиксное выражение инкремента/декремента вычисляет значение, которое было до его изменения. Это может привести к более компактному коду, но более сложному для чтения. Префиксная форма, как правило, более читабельна, никогда не бывает менее эффективной и может быть более эффективной, поскольку ей не нужно делать копию значения, которое было до операции.
Минусы:
В языке C сложилась традиция использования пост-инкремента, даже если значение выражения не используется, особенно в циклах for.
Решение:
Используйте префиксный инкремент/декремент, если только коду явно не нужен результат постфиксного выражения инкремента/декремента.
Использование констант
В API используйте const везде, где это имеет смысл. В некоторых случаях лучшим выбором будет constexpr.
Определение:
Объявленные переменные и параметры могут предваряться ключевым словом const, чтобы указать, что переменные не изменяются (например, const int32_t foo). Функции класса могут иметь квалификатор const, чтобы указать, что функция не изменяет состояние переменных-членов класса (например, class Foo { int32_t Bar(int8_t c) const; };).
Плюсы:
Людям проще понять, как используются переменные. Позволяет компилятору лучше проверять типы и, предположительно, генерировать лучший код. Помогает людям убедиться в правильности программы, поскольку они знают, что вызываемые ими функции ограничены в том, как они могут изменять ваши переменные. Помогает людям узнать, какие функции безопасно использовать без блокировок в многопоточных программах.
Минусы:
const вирусен: если вы передаете константную переменную в функцию, эта функция должна иметь const в своем прототипе (или переменной потребуется const_cast). Это может стать особой проблемой при вызове библиотечных функций.
Решение:
Мы настоятельно рекомендуем использовать const в API (т.е. в параметрах функций, методах и нелокальных переменных) везде, где это имеет смысл и точно. Это обеспечивает согласованную, в основном проверенную компилятором документацию о том, какие объекты операция может мутировать. Наличие согласованного и надежного способа отличать чтение от записи имеет решающее значение для написания потокобезопасного кода и полезно во многих других контекстах. В частности:
- Если функция гарантирует, что она не изменит аргумент, переданный по ссылке или указателю, соответствующий параметр функции должен быть ссылкой на константу (const T &) или указателем на константу (const T *) соответственно.
- Для параметра функции, переданного по значению, const не оказывает никакого влияния на вызывающую сторону, но помогает в исключение ошибок путем случайного переопределения входных данных.
- Объявляйте методы как константные, если только они не изменяют логическое состояние объекта (или не позволяют пользователю изменять это состояние, например, возвращая неконстантную ссылку, но это бывает редко), или их нельзя безопасно вызывать одновременно.
Использование const для локальных переменных не поощряется и не осуждается.
Все операции const класса должны быть безопасными для одновременного вызова друг с другом. Если это невозможно, класс должен быть четко задокументирован как «небезопасный для потоков».
Где разместить константу
Некоторые предпочитают форму int const * foo вместо const int * foo. Они утверждают, что это более читабельно, потому что более последовательно: это сохраняет правило, что const всегда следует за объектом, который он описывает. Однако этот аргумент о согласованности не применяется в кодовых базах с небольшим количеством глубоко вложенных выражений указателей, поскольку большинство выражений const имеют только один const, и он применяется к базовому значению. В таких случаях нет необходимости поддерживать согласованность. Размещение const первым, возможно, более читабельно, поскольку это следует английскому языку, помещая «прилагательное» (const) перед «существительным» (int).
Тем не менее, хотя мы и призываем ставить const первым, мы не требуем этого. Но будьте последовательны с кодом вокруг вас!
Использование constexpr, constinit и consteval
Используйте constexpr для определения истинных констант или для обеспечения постоянной инициализации. Используйте constinit для обеспечения постоянной инициализации для неконстантных переменных.
Определение:
Некоторые переменные могут быть объявлены как constexpr, чтобы указать, что переменные являются истинными константами, т.е. зафиксированы во время компиляции/линковки. Некоторые функции и конструкторы могут быть объявлены как constexpr, что позволяет использовать их при определении переменной constexpr. Функции могут быть объявлены как consteval, чтобы ограничить их использование временем компиляции.
Плюсы:
Использование constexpr позволяет определять константы с помощью выражений с плавающей точкой, а не только литералов; определять константы пользовательских типов; и определять константы с помощью вызовов функций.
Минусы:
Преждевременная маркировка чего-либо как constexpr может вызвать проблемы с миграцией, если впоследствии это придется понизить. Текущие ограничения на то, что разрешено в функциях и конструкторах constexpr, могут привести к неясным обходным путям в этих определениях.
Решение:
Определения constexpr позволяют более надежно специфицировать постоянные части интерфейса. Используйте constexpr для указания истинных констант и функций, которые поддерживают их определения. consteval можно использовать для кода, который не должен вызываться во время выполнения. Избегайте усложнения определений функций, чтобы сделать возможным их использование с constexpr. Не используйте constexpr или consteval для принудительного встраивания.
Целочисленные типы
Из встроенных целочисленных типов C++ используется только int. Если программе требуется целочисленный тип другого размера, используйте целочисленный тип с точной шириной из , например int16_t. Если у вас есть значение, которое может быть больше или равно 2^31, используйте 64-битный тип, например int64_t. Помните, что даже если ваше значение никогда не будет слишком большим для int, оно может использоваться в промежуточных вычислениях, для которых может потребоваться больший тип. Если вы сомневаетесь, выбирайте больший тип.
Определение:
C++ не определяет точные размеры для целочисленных типов, таких как int. Обычные размеры на современных архитектурах составляют 16 бит для short, 32 бита для int, 32 или 64 бита для long и 64 бита для long long, но разные платформы делают разные выборы, в частности для long.
Плюсы:
Единообразие декларации.
Минусы:
Размеры целочисленных типов в C++ могут различаться в зависимости от компилятора и архитектуры.
Решение:
Заголовок стандартной библиотеки определяет такие типы, как int16_t, uint32_t, int64_t и т.д. Вы всегда должны использовать их вместо short, unsigned long long и т.п., когда вам нужна гарантия размера целого числа. Предпочитайте опускать префикс std:: для этих типов, так как дополнительные 5 символов не оправдывают добавленного беспорядка. Из встроенных целочисленных типов следует использовать только int. Когда это уместно, вы можете использовать стандартные псевдонимы типов, такие как size_t и ptrdiff_t.
Мы используем int очень часто для целых чисел, которые, как мы знаем, не будут слишком большими, например, счетчики циклов. Используйте для таких вещей обычный старый int. Вы должны предполагать, что int имеет длину не менее 32 бит, но не предполагайте, что он имеет более 32 бит. Если вам нужен 64-битный целочисленный тип, используйте int64_t или uint64_t.
Для целых чисел, которые, как мы знаем, могут быть «большими», используйте int64_t.
Не следует использовать типы беззнаковых целых чисел, такие как uint32_t, если только нет веской причины, например, представления битовой последовательности, а не числа, или вам не нужно определить переполнение по модулю 2^N. В частности, не используйте типы беззнаковых чисел, чтобы сказать, что число никогда не будет отрицательным. Вместо этого используйте для этого утверждения.
Если ваш код представляет собой контейнер, который возвращает размер, обязательно используйте тип, который будет соответствовать любому возможному использованию вашего контейнера. Если вы сомневаетесь, используйте больший тип, а не меньший.
Будьте осторожны при преобразовании целочисленных типов. Преобразования и повышения целых чисел могут привести к неопределенному поведению, что приведет к ошибкам безопасности и другим проблемам.
О беззнаковых целых числах
Беззнаковые целые числа хороши для представления битовых полей и модульной арифметики. Из-за исторической случайности стандарт C++ также использует беззнаковые целые числа для представления размера контейнеров — многие члены комитета по стандартизации считают это ошибкой, но на данный момент её фактически невозможно исправить. Тот факт, что беззнаковая арифметика не моделирует поведение простого целого числа, а вместо этого определена стандартом для моделирования модульной арифметики (переход на переполнении/незаполнении), означает, что значительный класс ошибок не может быть диагностирован компилятором. В других случаях определенное поведение препятствует оптимизации.
Тем не менее, смешивание знаковости целочисленных типов несет ответственность за столь же большой класс проблем. Лучший совет, который мы можем дать: старайтесь использовать итераторы и контейнеры вместо указателей и размеров, старайтесь не смешивать знаковость и старайтесь избегать беззнаковых типов (за исключением представления битовых полей или модульной арифметики). Не используйте беззнаковый тип просто для утверждения, что переменная неотрицательна.
Типы с плавающей точкой
Из встроенных типов с плавающей точкой C++ используются только float и double. Вы можете предположить, что эти типы представляют IEEE-754 binary32 и binary64 соответственно.
Не используйте long double, так как это дает непереносимые результаты.
Архитектура Переносимости
Пишите архитектурно-переносимый код. Не полагайтесь на возможности ЦП, специфичные для одного процессора.
- При печати значений используйте библиотеки типобезопасного числового форматирования, такие как absl::StrCat, absl::Substitute, absl::StrFormat или std::ostream, вместо семейства функций printf.
- При перемещении структурированных данных в процесс или из него кодируйте их с помощью библиотеки сериализации, такой как Protocol Buffers, а не копируйте представление в памяти.
- Если вам нужно работать с адресами памяти как с целыми числами, сохраняйте их в uintptr_ts, а не в uint32_ts или uint64_ts.
- Используйте braced-initialization по мере необходимости для создания 64-битных констант. Например:
int64_t my_value{0x123456789};
uint64_t my_mask{uint64_t{3} << 48};
- Используйте переносимые типы с плавающей точкой; избегайте long double.
- Используйте переносимые целочисленные типы; избегайте short, long и long long.
Макросы препроцессора
Избегайте определения макросов, особенно в заголовках; предпочитайте встроенные функции, перечисления и константные переменные. Называйте макросы префиксом, специфичным для проекта. Не используйте макросы для определения частей API C++.
Макросы означают, что код, который вы видите, отличается от кода, который видит компилятор. Это может привести к неожиданному поведению, особенно потому, что макросы имеют глобальную область действия.
Проблемы, создаваемые макросами, особенно серьезны, когда они используются для определения частей API C++, и еще больше для публичных API. Каждое сообщение об ошибке от компилятора, когда разработчики неправильно используют этот интерфейс, теперь должно объяснять, как макросы сформировали интерфейс. Инструментам рефакторинга и анализа стало значительно сложнее обновлять интерфейс. Как следствие, мы специально запрещаем использовать макросы таким образом. Например, избегайте таких шаблонов, как:
class WOMBAT_TYPE(Foo) {
// ...
public:
EXPAND_PUBLIC_WOMBAT_API(Foo)
EXPAND_WOMBAT_COMPARISONS(Foo, ==, <)
};
К счастью, макросы в C++ не так необходимы, как в C. Вместо использования макроса для встраивания критического для производительности кода используйте встроенную функцию. Вместо использования макроса для хранения константы используйте константную переменную. Вместо использования макроса для «сокращения» длинного имени переменной используйте ссылку. Вместо использования макроса для условной компиляции кода… ну, не делайте этого вообще (кроме, конечно, защитных конструкций #define для предотвращения двойного включения заголовочных файлов). Это значительно усложняет тестирование.
Макросы могут делать то, чего не могут другие методы, и вы видите их в кодовой базе, особенно в библиотеках нижнего уровня. И некоторые из их специальных функций (такие как стрингификация, конкатенация и т.д.) недоступны через сам язык. Но перед использованием макроса тщательно подумайте, нет ли способа без макросов достичь того же результата. Если вам нужно использовать макрос для определения интерфейса, свяжитесь с руководителями вашего проекта, чтобы запросить отказ от этого правила.
Следующая схема использования позволит избежать многих проблем с макросами; если вы используете макросы, следуйте ей по возможности:
- Не определяйте макросы в файле .hpp.
- #define макросы непосредственно перед их использованием и #undef их сразу после.
- Не просто #undef существующий макрос перед заменой его своим собственным; вместо этого выберите имя, которое, скорее всего, будет уникальным.
- Постарайтесь не использовать макросы, которые расширяются до несбалансированных конструкций C++, или, по крайней мере, хорошо документируйте такое поведение.
- Предпочитайте не использовать ## для генерации имен функций/классов/переменных.
Экспорт макросов из заголовков (т.е. определение их в заголовке без #undefing их перед концом заголовка) крайне не рекомендуется. Если вы экспортируете макрос из заголовка, у него должно быть глобально уникальное имя. Для этого его имя должно быть названо с префиксом, состоящим из имени пространства имён вашего проекта (но в верхнем регистре).
0 and nullptr/NULL
Используйте nullptr для указателей и ‘\0’ для символов (а не литерал 0).
Для указателей (адресных значений) используйте nullptr, так как это обеспечивает безопасность типов.
Используйте ‘\0’ для символа null. Использование правильного типа делает код более читабельным.
sizeof
Предпочитайте sizeof(varname) sizeof(type).
Используйте sizeof(varname), когда берете размер определенной переменной. sizeof(varname) обновится соответствующим образом, если кто-то изменит тип переменной сейчас или позже. Вы можете использовать sizeof(type) для кода, не связанного с какой-либо конкретной переменной, например, кода, который управляет внешним или внутренним форматом данных, где переменная соответствующего типа C++ неудобна.
// Правильно
MyStruct data;
::memset(&data, 0, sizeof(data));
// Не правильно
::memset(&data, 0, sizeof(MyStruct));
if(raw_size < sizeof(int32_t)){
LOG(ERROR) << "Compressed record not big enough for count: " << raw_size;
return false;
}
Исключение типа (включая авто)
Используйте вывод типа только в том случае, если это делает код более понятным для читателей, которые не знакомы с проектом, или если это делает код более безопасным. Не используйте его только для того, чтобы избежать неудобств, связанных с написанием явного типа.
Определение:
Существует несколько контекстов, в которых C++ позволяет (или даже требует), чтобы типы выводились компилятором, а не прописывались явно в коде:
Вывод аргумента шаблона функции Шаблон функции может быть вызван без явных аргументов шаблона. Компилятор выводит эти аргументы из типов аргументов функции:
template <typename T>
void f(T t);
f(0); // Invokes f <int> (0)
Объявления переменных auto Объявление переменной может использовать ключевое слово auto вместо типа. Компилятор выводит тип из инициализатора переменной, следуя тем же правилам, что и вывод аргумента шаблона функции с тем же инициализатором (если только вы не используете фигурные скобки вместо круглых скобок).
auto a = 42; // a is an int
auto & b = a; // b is an int&
auto c = b; // c is an int
auto d{42}; // d is an int, not a std::initializer_list <int>
auto может быть квалифицировано с помощью const и может использоваться как часть указателя или ссылочного типа, а также (начиная с C++17) как нетиповой аргумент шаблона. Редкий вариант этого синтаксиса использует decltype(auto) вместо auto, в этом случае выведенный тип является результатом применения decltype к инициализатору. Вывод возвращаемого типа функции auto (и decltype(auto)) также могут использоваться вместо возвращаемого типа функции. Компилятор выводит возвращаемый тип из операторов return в теле функции, следуя тем же правилам, что и для объявлений переменных:
auto f(){ return 0; } // Тип возвращаемого значения f — int.
Типы возвращаемых значений лямбда-выражений могут быть выведены таким же образом, но это происходит из-за пропуска типа возвращаемого значения, а не явного auto. Сбивает с толку то, что синтаксис завершающего типа возвращаемого значения для функций также использует auto в позиции типа возвращаемого значения, но это не зависит от вывода типа; это просто альтернативный синтаксис для явного типа возвращаемого значения. Универсальные лямбды Лямбда-выражение может использовать ключевое слово auto вместо одного или нескольких типов параметров. Это приводит к тому, что оператор вызова лямбды становится шаблоном функции вместо обычной функции с отдельным параметром шаблона для каждого параметра функции auto:
// Сортировка `vec` в порядке убывания
std::sort(vec.begin(), vec.end(), [](auto lhs, auto rhs) { return lhs > rhs; });
Захваты лямбда-инициализации Лямбда-захваты могут иметь явные инициализаторы, которые можно использовать для объявления совершенно новых переменных, а не только для захвата существующих:
[x = 42, y = "foo"] { ... } // x — это int, а y — это const char *
Этот синтаксис не позволяет указывать тип; вместо этого он выводится с использованием правил для переменных auto. Вывод аргумента шаблона класса См. ниже. Структурированные привязки При объявлении кортежа, структуры или массива с использованием auto вы можете указать имена для отдельных элементов вместо имени для всего объекта; эти имена называются «структурированными привязками», а все объявление называется «объявлением структурированного привязки». Этот синтаксис не предоставляет способа указать тип ни включающего объекта, ни отдельных имён:
auto [iter, success] = my_map.insert({key, value});
if(!success)
iter->second = value;
auto также может быть квалифицирован с помощью const, & и &&, но обратите внимание, что эти квалификаторы технически применяются к анонимному кортежу/структуре/массиву, а не к отдельным привязкам. Правила, определяющие типы привязок, довольно сложны; результаты, как правило, неудивительны, за исключением того, что типы привязок обычно не будут ссылками, даже если объявление объявляет ссылку (но они, как правило, в любом случае будут вести себя как ссылки). (Эти сводки опускают многие детали и оговорки; см. ссылки для получения дополнительной информации.)
Плюсы:
- Имена типов C++ могут быть длинными и громоздкими, особенно когда они включают шаблоны или пространства имён.
- Когда имя типа C++ повторяется в одном объявлении или небольшом участке кода, повторение может не способствовать читабельности.
- Иногда безопаснее позволить вывести тип, поскольку это исключает возможность непреднамеренного копирования или преобразования типов.
Минусы:
Код C++ обычно более понятен, когда типы явные, особенно когда вывод типа зависит от информации из отдаленных частей кода. В выражениях типа:
auto foo = x.add_foo();
auto i = y.Find(key);
может быть неочевидно, какие типы будут получены, если тип y не очень хорошо известен или если y был объявлен на много строк раньше.
Программисты должны понимать, когда вывод типа даст или не даст ссылочный тип, иначе они получат копии, когда не хотели этого.
Если выводимый тип используется как часть интерфейса, то программист может изменить его тип, намереваясь изменить только его значение, что приведет к более радикальному изменению API, чем предполагалось.
Решение:
Основное правило: используйте вывод типа только для того, чтобы сделать код более понятным или безопасным, и не используйте его просто для того, чтобы избежать неудобств, связанных с написанием явного типа. При оценке того, стал ли код более понятным, помните, что ваши читатели не обязательно находятся в вашей команде или знакомы с вашим проектом, поэтому типы, которые вы и ваш рецензент воспринимаете как ненужный беспорядок, очень часто будут предоставлять полезную информацию другим. Например, вы можете предположить, что возвращаемый тип make_unique () очевиден, но возвращаемый тип MyWidgetFactory(), вероятно, нет.
Эти принципы применимы ко всем формам вывода типа, но детали различаются, как описано в следующих разделах.
Вывод аргумента шаблона функции
Вывод аргумента шаблона функции почти всегда ОК. Вывод типа — ожидаемый способ взаимодействия с шаблонами функций по умолчанию, поскольку он позволяет шаблонам функций действовать как бесконечные наборы обычных перегрузок функций. Следовательно, шаблоны функций почти всегда разрабатываются так, чтобы вывод аргумента шаблона был понятным и безопасным или не компилировался.
Вывод типа локальной переменной
Для локальных переменных можно использовать вывод типа, чтобы сделать код более понятным, исключив очевидную или нерелевантную информацию о типе, чтобы читатель мог сосредоточиться на значимых частях кода:
std::unique_ptr <WidgetWithBellsAndWhistles> widget = std::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, 6> numbers = {4, 8, 15, 16, 23, 42};
auto widget = std::make_unique <WidgetWithBellsAndWhistles> (arg1, arg2);
auto it = my_map_.find(key);
std::array numbers = {4, 8, 15, 16, 23, 42};
Типы иногда содержат смесь полезной информации и шаблона, как в примере выше: очевидно, что тип является итератором, и во многих контекстах тип контейнера и даже тип ключа не имеют значения, но тип значений, вероятно, полезен. В таких ситуациях часто можно определить локальные переменные с явными типами, которые передают соответствующую информацию:
if(auto it = my_map_.find(key); it != my_map_.end()){
WidgetWithBellsAndWhistles& widget = (* it->second);
// Делайте что-нибудь с `widget`
}
Если тип является экземпляром шаблона, а параметры являются шаблонными, но сам шаблон информативен, вы можете использовать вывод аргумента шаблона класса для подавления шаблона. Однако случаи, когда это действительно дает значимую выгоду, довольно редки. Обратите внимание, что вывод аргумента шаблона класса также подчиняется отдельному правилу стиля.
Не используйте decltype(auto), если подойдет более простой вариант, потому что это довольно неясная функция, поэтому она имеет высокую цену в ясности кода.
Вычитание возвращаемого типа
Используйте вывод возвращаемого типа (как для функций, так и для лямбда-выражений) только в том случае, если тело функции имеет очень небольшое количество операторов возврата и очень мало другого кода, поскольку в противном случае читатель может не сразу определить возвращаемый тип. Кроме того, используйте его только в том случае, если функция или лямбда имеют очень узкую область действия, поскольку функции с выводимыми возвращаемыми типами не определяют границы абстракции: реализация — это интерфейс. В частности, публичные функции в заголовочных файлах почти никогда не должны иметь выводимых возвращаемых типов.
Вывод типа параметра
Типы параметров auto для лямбда-выражений следует использовать с осторожностью, поскольку фактический тип определяется кодом, вызывающим лямбда-выражение, а не определением лямбда-выражения. Следовательно, явный тип почти всегда будет понятнее, если только лямбда-выражение явно не вызывается очень близко к месту его определения (чтобы читатель мог легко увидеть и то, и другое), или лямбда-выражение не передается в интерфейс, настолько известный, что очевидно, с какими аргументами оно в конечном итоге будет вызвано (например, пример std::sort выше).
Захват инициализации лямбды
На захваты init распространяется более конкретное правило стиля, которое в значительной степени заменяет общие правила вывода типа.
Структурированные переплёты
В отличие от других форм вывода типа, структурированные привязки могут фактически предоставить читателю дополнительную информацию, давая осмысленные имена элементам более крупного объекта. Это означает, что структурированное объявление привязки может обеспечить общее улучшение читаемости по сравнению с явным типом, даже в тех случаях, когда auto этого не сделает. Структурированные привязки особенно полезны, когда объект является парой или кортежем (как в примере вставки выше), потому что у них изначально нет осмысленных имён полей, но обратите внимание, что обычно вам не следует использовать пары или кортежи, если только существующий API, такой как вставка, не заставляет вас этого делать.
Если привязываемый объект является структурой, иногда может быть полезно предоставить имена, которые более специфичны для вашего использования, но имейте в виду, что это также может означать, что имена будут менее узнаваемы для вашего читателя, чем имена полей. Мы рекомендуем использовать комментарий, чтобы указать имя базового поля, если оно не совпадает с именем привязки, используя тот же синтаксис, что и для комментариев параметров функции:
auto [/*field_name1=*/bound_name1, /*field_name2=*/bound_name2] = ...
Как и в случае с комментариями параметров функции, это позволяет инструментам определять, не указан ли неправильный порядок полей.
Вывод аргумента шаблона класса
Используйте вывод аргументов шаблона класса только с шаблонами, которые явно поддержали его.
Определение:
Вывод аргумента шаблона класса (часто сокращенно «CTAD») происходит, когда переменная объявлена с типом, который именует шаблон, а список аргументов шаблона не указан (даже пустые угловые скобки):
std::array a = {1, 2, 3}; // `a` — это std::array <int, 3>
Компилятор выводит аргументы из инициализатора, используя «руководства по выводам» шаблона, которые могут быть явными или неявными.
Явные руководства по выводам выглядят как объявления функций с конечными возвращаемыми типами, за исключением того, что нет начального auto, а имя функции является именем шаблона. Например, приведенный выше пример опирается на это руководство по выводам для std::array:
namespace std {
template <class T, class... U>
array(T, U...) -> std::array <T, 1 + sizeof...(U)>;
}
Конструкторы в первичном шаблоне (в отличие от специализации шаблона) также неявно определяют руководства по выводу.
Когда вы объявляете переменную, которая опирается на CTAD, компилятор выбирает руководство по выводу, используя правила разрешения перегрузки конструктора, и возвращаемый тип этого руководства становится типом переменной.
Плюсы:
Иногда CTAD позволяет исключить шаблонный код из кода.
Минусы:
Неявные руководства по выводу, которые генерируются из конструкторов, могут иметь нежелательное поведение или быть совершенно неверными. Это особенно проблематично для конструкторов, написанных до того, как CTAD был представлен в C++17, поскольку авторы этих конструкторов не имели возможности узнать о (не говоря уже об исправлении) любых проблемах, которые их конструкторы могли бы вызвать для CTAD. Более того, добавление явных руководств по выводу для исправления этих проблем может сломать любой существующий код, который полагается на неявные руководства по выводу.
CTAD также страдает от многих из тех же недостатков, что и auto, поскольку они оба являются механизмами для выведения всего или части типа переменной из её инициализатора. CTAD действительно дает читателю больше информации, чем auto, но он также не даёт читателю очевидного намёка на то, что информация была пропущена.
Решение:
Не используйте CTAD с заданным шаблоном, если только сопровождающие шаблона не согласились поддержать использование CTAD, предоставив по крайней мере одно явное руководство по выводу (все шаблоны в пространстве имён std также предполагают, что согласились). Это должно быть реализовано с предупреждением компилятора, если оно доступно.
Использование CTAD также должно соответствовать общим правилам вывода типа.
Назначенные инициализаторы
Используйте назначенные инициализаторы только в форме, совместимой с C++20.
Назначенные инициализаторы — это синтаксис, позволяющий инициализировать агрегат («обычную старую структуру»), явно называя его поля:
struct Point {
float x = .0f;
float y = .0f;
float z = .0f;
};
Point p = {
.x = 1.f,
.y = 2.f,
// z будет .0f
};
Явно перечисленные поля будут инициализированы так, как указано, а другие будут инициализированы так же, как и в традиционном выражении инициализации агрегации, например Point {1., 2.}.
Плюсы:
Назначенные инициализаторы могут обеспечить удобные и легко читаемые агрегатные выражения, особенно для структур с менее простым порядком полей, чем в примере Point выше.
Минусы:
Хотя назначенные инициализаторы давно являются частью стандарта C и поддерживаются компиляторами C++ как расширение, они не поддерживались C++ до C++20.
Правила в стандарте C++ строже, чем в C и расширениях компилятора, требуя, чтобы назначенные инициализаторы появлялись в том же порядке, в котором поля появляются в определении структуры. Таким образом, в приведенном выше примере, согласно C++20, допустимо инициализировать x, а затем z, но не y, а затем x.
Решение:
Используйте назначенные инициализаторы только в форме, совместимой со стандартом C++20: с инициализаторами в том же порядке, в котором соответствующие поля появляются в определении структуры.
Лямбда-выражения
Используйте лямбда-выражения, где это уместно. Предпочитайте явные захваты, когда лямбда выйдет за пределы текущей области видимости.
Определение:
Лямбда-выражения — это краткий способ создания анонимных объектов функций. Они часто полезны при передаче функций в качестве аргументов. Например:
std::sort(v.begin(), v.end(), [](int x, int y){
return Weight(x) < Weight(y);
});
Они также позволяют захватывать переменные из охватывающей области видимости либо явно по имени, либо неявно с использованием захвата по умолчанию. Явные захваты требуют, чтобы каждая переменная была указана, либо как значение, либо как захват ссылки:
int32_t sum = 0;
int32_t weight = 3;
// Фиксирует `weight` по значению и `sum` по ссылке.
std::for_each(v.begin(), v.end(), [weight, &sum](int x){
sum += weight * x;
});
Захваты по умолчанию неявно захватывают любую переменную, на которую ссылается тело лямбда-выражения, включая эту, если используются какие-либо члены:
std::vector <int32_t> indices = ...;
const std::vector <int32_t> lookup_table = ...;
// Захватывает `lookup_table` по ссылке, сортирует `indexes` по значению связанного элемента в `lookup_table`.
std::sort(indices.begin(), indices.end(), [&](int32_t a, int32_t b){
return lookup_table[a] < lookup_table[b];
});
Захват переменной также может иметь явный инициализатор, который может использоваться для захвата переменных, предназначенных только для перемещения, по значению или для других ситуаций, не обрабатываемых обычными захватами ссылок или значений:
std::unique_ptr <Foo> foo = ...;
[foo = std::move(foo)](){
...
}
Такие захваты (часто называемые «захватами init» или «обобщенными захватами лямбда») на самом деле не должны «захватывать» что-либо из охватывающей области действия или даже иметь имя из охватывающей области действия; этот синтаксис является полностью общим способом определения членов лямбда-объекта:
[foo = std::vector <int32_t> ({1, 2, 3})](){
...
}
Тип захвата с инициализатором выводится по тем же правилам, что и auto.
Плюсы:
- Лямбда-выражения гораздо более лаконичны, чем другие способы определения объектов функций для передачи в алгоритмы STL, что может улучшить читаемость.
- Правильное использование захватов по умолчанию может устранить избыточность и выделить важные исключения из значений по умолчанию.
- Лямбда-выражения, std::function и std::bind можно использовать в сочетании как универсальный механизм обратного вызова; они упрощают написание функций, которые принимают связанные функции в качестве аргументов.
Минусы:
- Захват переменной в лямбдах может быть источником ошибок с висячими указателями, особенно если лямбда выходит из текущей области видимости.
- Захваты по умолчанию по значению могут вводить в заблуждение, поскольку они не предотвращают ошибки с висячими указателями. Захват указателя по значению не вызывает глубокого копирования, поэтому он часто имеет те же проблемы с жизненным циклом, что и захват по ссылке. Это особенно сбивает с толку при захвате по значению, поскольку использование this часто неявно.
- Захваты фактически объявляют новые переменные (независимо от того, есть ли у захватов инициализаторы), но они совсем не похожи на любой другой синтаксис объявления переменных в C++. В частности, нет места для типа переменной или даже заполнителя auto (хотя захваты init могут указывать на него косвенно, например, с помощью приведения). Это может даже затруднить их распознавание как объявлений.
- Init captures по своей сути полагаются на вывод типа и страдают от многих из тех же недостатков, что и auto, с дополнительной проблемой, что синтаксис даже не намекает читателю, что вывод происходит. Использование лямбда-выражений может выйти из-под контроля; очень длинные вложенные анонимные функции могут усложнить понимание кода.
Решение:
- Используйте лямбда-выражения, где это уместно, с форматированием, как описано ниже.
- Предпочитайте явные захваты, если лямбда может выйти за пределы текущей области видимости. Например, вместо:
{
Foo foo;
...
executor->Schedule([&]{
Frobnicate(foo);
})
...
}
/**
* ПЛОХО! Тот факт, что лямбда использует ссылку на `foo` и, возможно, `this` (если `Frobnicate` является функцией-членом), может быть неочевиден при поверхностном осмотре.
* Если лямбда-функция вызывается после возврата функции, это будет плохо, поскольку и `foo`, и содержащий его объект могут быть уничтожены.
*/
предпочитаю писать:
{
Foo foo;
...
executor->Schedule([&foo]{
Frobnicate(foo);
})
...
}
// ЛУЧШЕ - Компиляция завершится ошибкой, если `Frobnicate` является функцией-членом, и становится яснее, что `foo` опасно захвачен ссылкой.
- Используйте захват по умолчанию по ссылке ([&]) только тогда, когда время жизни лямбды очевидно короче любых потенциальных захватов.
- Используйте захват по умолчанию по значению ([=]) только как средство связывания нескольких переменных для короткой лямбды, где набор захваченных переменных очевиден с первого взгляда, и который не приводит к захвату этого неявно. (Это означает, что лямбда, которая появляется в нестатической функции-члене класса и ссылается на нестатические члены класса в своем теле, должна захватывать это явно или через [&].) Предпочитайте не писать длинные или сложные лямбды с захватом по умолчанию по значению.
- Используйте захваты только для фактического захвата переменных из охватывающей области видимости. Не используйте захваты с инициализаторами для введения новых имен или для существенного изменения значения существующего имени. Вместо этого объявите новую переменную обычным способом, а затем захватите ее, или избегайте сокращения лямбды и явно определите объект функции.
- См. раздел о выводе типа для получения рекомендаций по указанию параметров и возвращаемых типов.
Шаблонное метапрограммирование
Избегайте сложного программирования шаблонов.
Определение:
Шаблонное метапрограммирование относится к семейству методов, которые используют тот факт, что механизм создания экземпляров шаблонов C++ является полным по Тьюрингу и может использоваться для выполнения произвольных вычислений во время компиляции в области типов.
Плюсы:
Шаблонное метапрограммирование позволяет создавать чрезвычайно гибкие интерфейсы, которые являются типобезопасными и высокопроизводительными. Такие возможности, как GoogleTest, std::tuple, std::function и Boost.Spirit, были бы невозможны без него.
Минусы:
Методы, используемые в метапрограммировании шаблонов, часто неясны никому, кроме экспертов по языку. Код, который использует шаблоны сложным образом, часто нечитаем, его трудно отлаживать или поддерживать.
Метапрограммирование шаблонов часто приводит к крайне плохим сообщениям об ошибках во время компиляции: даже если интерфейс прост, сложные детали реализации становятся видны, когда пользователь делает что-то неправильно.
Метапрограммирование шаблонов мешает масштабному рефакторингу, усложняя работу инструментов рефакторинга. Во-первых, код шаблона расширяется в нескольких контекстах, и трудно проверить, что преобразование имеет смысл во всех из них. Во-вторых, некоторые инструменты рефакторинга работают с AST, который представляет только структуру кода после расширения шаблона. Может быть сложно автоматически вернуться к исходной конструкции источника, которую необходимо переписать.
Решение:
Шаблонное метапрограммирование иногда позволяет создавать более чистые и простые в использовании интерфейсы, чем это было бы возможно без него, но также часто возникает соблазн быть слишком умным. Лучше всего его использовать в небольшом количестве низкоуровневых компонентов, где дополнительная нагрузка по обслуживанию распределяется на большое количество применений.
Подумайте дважды, прежде чем использовать шаблонное метапрограммирование или другие сложные шаблонные методы; подумайте, сможет ли среднестатистический член вашей команды понять ваш код достаточно хорошо, чтобы поддерживать его после того, как вы переключитесь на другой проект, или сможет ли программист, не владеющий C++, или кто-то, кто случайно просматривает базу кода, понять сообщения об ошибках или отследить поток функции, которую он хочет вызвать. Если вы используете рекурсивные инстанцирования шаблонов или списки типов или метафункции или шаблоны выражений, или полагаетесь на SFINAE или на трюк с sizeof для обнаружения разрешения перегрузки функций, то есть большая вероятность, что вы зашли слишком далеко.
Если вы используете шаблонное метапрограммирование, вы должны ожидать, что приложите значительные усилия для минимизации и изоляции сложности. Вы должны скрывать метапрограммирование как деталь реализации, когда это возможно, чтобы заголовки, обращенные к пользователю, были читаемыми, и вы должны убедиться, что сложный код особенно хорошо прокомментирован. Вы должны тщательно документировать, как используется код, и вы должны сказать что-то о том, как выглядит «сгенерированный» код. Уделяйте особое внимание сообщениям об ошибках, которые выдает компилятор, когда пользователи совершают ошибки. Сообщения об ошибках являются частью вашего пользовательского интерфейса, и ваш код должен быть настроен по мере необходимости, чтобы сообщения об ошибках были понятными и применимыми с точки зрения пользователя.
Концепции и ограничения
Используйте концепции экономно. В целом, концепции и ограничения следует использовать только в тех случаях, когда шаблоны использовались бы до C++20. Избегайте введения новых концепций в заголовки, если только заголовки не помечены как внутренние для библиотеки. Не определяйте концепции, которые не применяются компилятором. Предпочитайте ограничения метапрограммированию шаблонов и избегайте синтаксиса template ; вместо этого используйте синтаксис require(Concept ).
Определение:
Ключевое слово concept — это новый механизм определения требований (таких как характеристики типа или спецификации интерфейса) для параметра шаблона. Ключевое слово require предоставляет механизмы для размещения анонимных ограничений в шаблонах и проверки того, что ограничения удовлетворены во время компиляции. Концепции и ограничения часто используются вместе, но могут также использоваться независимо.
Плюсы:
- Концепции позволяют компилятору генерировать гораздо более качественные сообщения об ошибках при использовании шаблонов, что может уменьшить путаницу и значительно улучшить процесс разработки.
- Концепции могут сократить шаблон, необходимый для определения и использования ограничений времени компиляции, часто повышая ясность получаемого кода.
- Ограничения предоставляют некоторые возможности, которые трудно реализовать с помощью шаблонов и методов SFINAE.
Минусы:
- Как и в случае с шаблонами, концепции могут значительно усложнить код и сделать его трудным для понимания.
- Синтаксис концепций может сбивать с толку читателей, поскольку концепции кажутся похожими на типы классов в местах их использования.
- Концепции, особенно на границах API, увеличивают связанность кода, жесткость и окостенение.
- Концепции и ограничения могут копировать логику из тела функции, что приводит к дублированию кода и увеличению затрат на обслуживание.
- Концепции запутывают источник истины для своих базовых контрактов, поскольку они являются автономными именованными сущностями, которые могут использоваться в нескольких местах, каждое из которых развивается отдельно друг от друга. Это может привести к тому, что заявленные и подразумеваемые требования будут со временем расходиться.
- Концепции и ограничения влияют на разрешение перегрузки новыми и неочевидными способами.
- Как и в случае с SFINAE, ограничения затрудняют рефакторинг кода в масштабе.
Решение:
Предопределенные концепции в стандартной библиотеке должны быть предпочтительнее типовых черт, если существуют эквивалентные. (например, если std::is_integral_v использовался до C++20, то std::integral следует использовать в коде C++20.) Аналогично, предпочитайте современный синтаксис ограничений (через require(Condition)). Избегайте устаревших конструкций метапрограммирования шаблонов (таких как std::enable_if ), а также синтаксиса template .
Не переопределяйте вручную существующие концепции или черты. Например, используйте require(std::default_initializable ) вместо require(requires { T v; }) или тому подобного.
Новые декларации концепций должны быть редкими и определяться только внутри библиотеки, чтобы они не были видны на границах API. В более общем плане не используйте концепции или ограничения в случаях, когда вы не стали бы использовать их устаревшие эквиваленты шаблонов в C++17.
Не определяйте концепции, которые дублируют тело функции, или не налагайте требования, которые были бы незначительными или очевидными при чтении тела кода или полученных сообщений об ошибках. Например, избегайте следующего:
// Плохо — избыточно с незначительной выгодой
template <typename T>
concept Addable = std::copyable <T> && requires(T a, T b){ a + b; };
template <Addable T>
T Add(T x, T y, T z){ return x + y + z; }
Вместо этого лучше оставить код в виде обычного шаблона, если только вы не можете продемонстрировать, что концепции приводят к значительному улучшению для этого конкретного случая, например, в результирующих сообщениях об ошибках для глубоко вложенного или неочевидного требования.
Концепции должны быть статически проверяемыми компилятором. Не используйте никакие концепции, основные преимущества которых будут исходить от семантического (или иным образом невыполненного) ограничения. Требования, которые невыполнены во время компиляции, должны быть вместо этого введены с помощью других механизмов, таких как комментарии, утверждения или тесты.
Модули C++20
Не используйте модули C++20.
C++20 представляет «модули», новую языковую функцию, разработанную как альтернатива текстовому включению заголовочных файлов. Он вводит три новых ключевых слова для поддержки этого: module, export и *import.
Модули — это большой сдвиг в том, как пишется и компилируется C++, и мы все еще оцениваем, как они могут вписаться в экосистему C++ в будущем. Кроме того, в настоящее время они не очень хорошо поддерживаются системами сборки, компиляторами и другими инструментами и требуют дальнейшего изучения наилучших практик при их написании и использовании.
Сопрограммы
Не используйте сопрограммы (пока).
Не включайте заголовок и не используйте ключевые слова co_await, co_yield или co_return.
ПРИМЕЧАНИЕ: ожидается, что этот запрет будет временным, пока разрабатываются дальнейшие рекомендации.
Boost
Используйте только одобренные библиотеки из коллекции библиотек Boost.
Определение:
Коллекция библиотек Boost — это популярная коллекция рецензируемых, бесплатных библиотек C++ с открытым исходным кодом.
Плюсы:
Код Boost, как правило, очень высокого качества, широко переносим и заполняет множество важных пробелов в стандартной библиотеке C++, таких как свойства типов и улучшенные связыватели.
Минусы:
Некоторые библиотеки Boost поощряют методы кодирования, которые могут ухудшить читаемость, такие как метапрограммирование и другие продвинутые методы шаблонизации, а также чрезмерно «функциональный» стиль программирования.
Решение:
Чтобы поддерживать высокий уровень читаемости для всех участников, которые могут читать и поддерживать код, мы разрешаем только одобренный подмножество функций Boost. В настоящее время разрешены следующие библиотеки:
- Вызовы свойств из boost/call_traits.hpp
- Сжатая пара из boost/compressed_pair.hpp
- Библиотека Boost Graph (BGL) из boost/graph, за исключением сериализации (adj_list_serialize.hpp) и параллельных/распределенных алгоритмов и структур данных (boost/graph/parallel/* и boost/graph/distributed/*).
- Карта свойств из 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
- Bimap из boost/bimap
- Статистические распределения и функции из boost/math/distributions
- Специальные функции из boost/math/special_functions
- Функции поиска и минимизации корня из boost/math/tools
- Мультииндекс из boost/multi_index
- Куча из boost/heap
- Плоские контейнеры из Container: boost/container/flat_map и boost/container/flat_set
- Интрузивный из boost/intrusive.
- Библиотека boost/sort.
- Препроцессор из boost/preprocessor.
Мы активно рассматриваем возможность добавления других функций Boost в этот список, поэтому в будущем этот список может быть расширен.
Запрещенные стандартные библиотечные функции
Как и в случае с Boost, некоторые современные функции библиотеки C++ поощряют практику кодирования, которая затрудняет читаемость, например, удаляя проверенную избыточность (такую как имена типов), которая может быть полезна для читателей, или поощряя метапрограммирование шаблонов. Другие расширения дублируют функциональность, доступную через существующие механизмы, что может привести к путанице и затратам на преобразование.
Решение:
Следующие функции стандартной библиотеки C++ использовать нельзя:
- Рациональные числа времени компиляции () из-за опасений, что они привязаны к более шаблонному стилю интерфейса.
- Заголовки и <fenv.h>, поскольку многие компиляторы не поддерживают эти функции надежно.
- Заголовок , который не имеет достаточной поддержки для тестирования и страдает от присущих ему уязвимостей безопасности.
Нестандартные расширения
Нестандартные расширения C++ не могут использоваться, если не указано иное.
Определение:
Компиляторы поддерживают различные расширения, которые не являются частью стандартного C++. Такие расширения включают __attribute__ GCC, встроенные функции, такие как __builtin_prefetch или SIMD, #pragma, встроенный ассемблер, __COUNTER__, __PRETTY_FUNCTION__, составные выражения операторов (например, foo = ({ int32_t x; Bar(&x); x }), массивы переменной длины и alloca(), а также «оператор Элвиса» a?:b.
Плюсы:
- Нестандартные расширения могут предоставлять полезные функции, которых нет в стандартном C++.
- Важные рекомендации по производительности для компилятора можно указать только с помощью расширений.
Минусы:
- Нестандартные расширения не работают во всех компиляторах. Использование нестандартных расширений снижает переносимость кода.
- Даже если они поддерживаются во всех целевых компиляторах, расширения часто не очень хорошо определены, и могут быть тонкие различия в поведении между компиляторами.
- Нестандартные расширения добавляют к возможностям языка, которые читатель должен знать, чтобы понять код.
- Нестандартные расширения требуют дополнительной работы для переноса между архитектурами.
Решение:
Не используйте нестандартные расширения. Вы можете использовать оболочки переносимости, которые реализованы с использованием нестандартных расширений, при условии, что эти оболочки предоставляются назначенным заголовком переносимости для всего проекта.
Псевдонимы
Публичные псевдонимы предназначены для удобства пользователей API и должны быть четко документированы.
Определение:
Существует несколько способов создания имён, являющихся псевдонимами других сущностей:
using Bar = Foo;
// Но предпочитаю `using` в коде C++.
typedef Foo Bar;
using ::other_namespace::Foo;
// Создает псевдонимы для всех перечислителей в MyEnumType.
using enum MyEnumType;
В новом коде использование предпочтительнее typedef, поскольку оно обеспечивает более согласованный синтаксис с остальной частью C++ и работает с шаблонами.
Как и другие объявления, псевдонимы, объявленные в заголовочном файле, являются частью открытого API этого заголовка, если только они не находятся в определении функции, в закрытой части класса или в явно обозначенном внутреннем пространстве имен. Псевдонимы в таких областях или в файлах .cpp являются деталями реализации (потому что клиентский код не может ссылаться на них) и не ограничиваются этим правилом.
Плюсы:
- Псевдонимы могут улучшить читаемость, упрощая длинное или сложное имя.
- Псевдонимы могут уменьшить дублирование, называя в одном месте тип, который используется многократно в API, что может упростить изменение типа в дальнейшем.
Минусы:
- При размещении в заголовке, где клиентский код может ссылаться на них, псевдонимы увеличивают количество сущностей в API этого заголовка, увеличивая его сложность.
- Клиенты могут легко полагаться на непреднамеренные детали публичных псевдонимов, что затрудняет внесение изменений.
- Может возникнуть соблазн создать публичный псевдоним, который предназначен только для использования в реализации, не принимая во внимание его влияние на API или на удобство обслуживания.
- Псевдонимы могут создавать риск конфликтов имён
- Псевдонимы могут снижать читаемость, давая знакомой конструкции незнакомое имя
- Псевдонимы типов могут создавать неясный контракт API: неясно, гарантированно ли псевдоним будет идентичен типу, псевдоним которого он создаёт, будет иметь тот же API или будет использоваться только в определенных узких целях
Решение:
Не помещайте псевдоним в свой публичный API только для того, чтобы сэкономить на вводе в реализацию; делайте это только в том случае, если вы соглашаетесь, чтобы его использовали ваши клиенты.
При определении публичного псевдонима документируйте цель нового имени, включая то, гарантированно ли оно всегда будет совпадать с типом, к которому оно в данный момент привязано, или предполагается более ограниченная совместимость. Это позволяет пользователю узнать, может ли он рассматривать типы как заменяемые или необходимо соблюдать более конкретные правила, и может помочь реализации сохранить некоторую степень свободы для изменения псевдонима.
Не помещайте псевдонимы пространств имён в свой публичный API. (См. также Пространства имён).
Например, эти псевдонимы документируют, как они предназначены для использования в клиентском коде:
namespace mynamespace {
// Используется для хранения измерений. DataPoint может измениться с Bar * на какой-либо внутренний тип.
// Клиентский код должен рассматривать его как непрозрачный указатель.
using DataPoint = ::foo::Bar *;
// Набор измерений. Просто псевдоним для удобства пользователя.
using TimeSeries = std::unordered_set <DataPoint, std::hash <DataPoint>, DataPointComparator>;
} // namespace mynamespace
Эти псевдонимы не документируют предполагаемое использование, и половина из них не предназначены для использования клиентами:
namespace mynamespace {
// Плохо: ни в одном из них не указано, как их следует использовать.
using DataPoint = ::foo::Bar *;
// Плохо: только для удобства локальных данных
using ::std::hash;
// Плохо: только для удобства локальных данных
using ::std::unordered_set;
typedef unordered_set <DataPoint, hash <DataPoint>, DataPointComparator> TimeSeries;
} // namespace mynamespace
Однако локальные удобные псевдонимы допустимы в определениях функций, закрытых разделах классов, явно обозначенных внутренних пространствах имен и в файлах .cpp:
// В .cpp файле
using ::foo::Bar;
Операторы переключения
Если не обусловлено перечисляемым значением, операторы switch всегда должны иметь случай по умолчанию (в случае перечисляемого значения компилятор предупредит вас, если какие-либо значения не будут обработаны). Если случай по умолчанию никогда не должен выполняться, считайте это ошибкой. Например:
switch(var){
case 0: {
...
break;
}
case 1: {
...
break;
}
default: {
LOG(FATAL) << "Недопустимое значение в операторе switch: " << var;
}
}
Провал из одной метки case в другую должен быть аннотирован с помощью атрибута [[fallthrough]];. [[fallthrough]]; должен быть помещён в точку выполнения, где происходит провал на следующую метку case. Распространённым исключением являются последовательные метки case без промежуточного кода, в этом случае аннотация не нужна.
switch(x){
case 41: // Здесь аннотации не нужны.
case 43:
if(dont_be_picky){
// Используйте это вместо или вместе с аннотациями в комментариях.
[[fallthrough]];
} else {
CloseButNoCigar();
break;
}
case 42:
DoSomethingSpecial();
[[fallthrough]];
default:
DoSomethingGeneric();
break;
}
Операторы неиспользуемых переменных
Часто бывает так, что переменные которые передаются в качестве аргументов функции, не требуются в реализации кода. Избегайте конструкций блокировки неиспользуемых переменных в стиле C вроде таких как (void). Применяйте для этих целей модификатор [[maybe_unused]] в стиле C++, так ваш код станет современным и более понятным для других разработчиков.
int32_t test(const int32_t a, const int32_t b, const int32_t c){
(void) b; // ПЛОХО: Устаревшая конструкция!
return (a + c);
}
// ОТЛИЧНО: Всё чётко и понятно
int32_t test(const int32_t a, [[maybe_unused]] const int32_t b, const int32_t c){
return (a + c);
}
Нейминг
Самые важные правила согласованности — это те, которые управляют именованием. Стиль имени немедленно сообщает нам, к какому типу относится именованная сущность: тип, переменная, функция, константа, макрос и т.д., не требуя от нас поиска объявления этой сущности. Механизм сопоставления с образцом в нашем мозгу во многом полагается на эти правила именования.
Правила именования довольно произвольны, но мы считаем, что согласованность важнее индивидуальных предпочтений в этой области, поэтому независимо от того, считаете ли вы их разумными или нет, правила есть правила.
Общие правила именования
Оптимизируйте для удобства чтения, используя имена, которые будут понятны даже людям из другой команды.
Используйте имена, которые описывают цель или намерение объекта. Не беспокойтесь об экономии горизонтального пространства, так как гораздо важнее сделать ваш код сразу понятным новому читателю. Минимизируйте использование сокращений, которые, скорее всего, будут неизвестны кому-то за пределами вашего проекта (особенно акронимов и аббревиатур). Не сокращайте, удаляя буквы внутри слова. Как правило, сокращение, вероятно, приемлемо, если оно указано в Википедии. В общем, описательность должна быть пропорциональна области видимости имени. Например, n может быть хорошим именем в пределах функции из 5 строк, но в пределах класса оно, скорее всего, слишком расплывчато.
class MyClass {
private:
// Понятное значение в контексте
const int32_t _kMaxAllowedConnections = ...;
public:
int32_t countFooErrors(const std::vector <Foo> & foos){
// Понятное значение с учётом ограниченного объёма и контекста
int32_t n = 0;
for(const auto& foo : foos){
...
++n;
}
return n;
}
void doSomethingImportant(){
// Известная аббревиатура для полностью квалифицированного доменного имени.
std::string fqdn = ...;
}
};
class MyClass {
private:
// Непонятное значение в широком смысле
const int32_t _kNum = ...;
public:
int32_t countFooErrors(const std::vector<Foo> & foos){
// Излишне многословно, учитывая ограниченный объём и контекст
int32_t total_number_of_foo_errors = 0;
// Используйте идиоматическое `i` (а для вложенных циклов j, k и т.д. ...)
for(int32_t foo_index = 0; foo_index < foos.size(); ++foo_index){
...
++total_number_of_foo_errors;
}
return total_number_of_foo_errors;
}
void doSomethingImportant(){
// Удаление внутренних символов
int32_t cstmr_id = ...;
}
};
Обратите внимание, что допустимы некоторые общеизвестные сокращения, например i для переменной итерации (для дочерних циклов j, k, …) и T для параметра шаблона.
Для целей правил именования ниже «word» — это всё, что вы написали бы на английском языке без внутренних пробелов. Это включает в себя сокращения, такие как аббревиатуры и инициализмы. Для имён, написанных в смешанном регистре (иногда называемом «camel case» или «Pascal case»), в которых первая буква каждого последующего слова пишется с заглавной буквы, а самая первая буква всегда с строчной буквы, предпочтительнее писать сокращения с заглавной буквы как отдельные слова, например, startRpc() вместо StartRPC(). Избегайте составных названий из нескольких слов разделённых дефизами или нижними поддчёркиваниями вроде этого: start_rpc()
Параметры шаблона должны следовать стилю именования для своей категории: параметры шаблона типа должны следовать правилам для имён типов, а параметры шаблона нетипа должны следовать правилам для имён переменных.
Имена файлов
Имена файлов должны быть написаны строчными буквами и могут включать подчеркивания (_) или тире (-). Следуйте соглашениям, принятым в вашем проекте. Если нет единого локального шаблона, которому можно следовать, предпочитайте “_”.
Примеры допустимых имён файлов:
my_useful_class.cpp
my-useful-class.cpp
myUsefulClass.cpp
myUsefulClass_test.cpp // _unittest и _regtest устарели.
Файлы C++ должны иметь расширение имени файла .cpp, а файлы заголовков должны иметь расширение .hpp. Файлы, которые зависят от текстового включения в определенных точках, должны заканчиваться на .inc (см. также раздел о самостоятельных заголовках).
Не используйте имена файлов, которые уже существуют в /usr/include, например, db.h.
В общем, делайте имена файлов очень конкретными. Например, используйте httpServerLogs.hpp вместо logs.h. Очень распространенный случай — иметь пару файлов с именами, например, fooBar.hpp и fooBar.cpp, определяющих класс с именем FooBar.
Названия типов
Названия структур и классов начинаются с заглавной буквы и имеют заглавную букву для каждого нового слова, без подчеркивания: MyExcitingClass, MyExcitingStruct.
Названия всех типов данных — имеют одинаковое соглашение об именовании. Названия типов данных, даже тех, что создаются с помощью модификатора typedef и using должны быть всегда в нижнем регистре, составные слова должны разделяться нижним подчёркиванием _ и заканчиваться суффиксом _t Например:
// Классы и структуры
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...
// typedefs
typedef hash_map <UrlTableProperties *, std::string> properties_map_t;
// Использование псевдонимов
using properties_map_t = hash_map <UrlTableProperties *, std::string>;
// enums
enum class url_table_error_t : uint8_t { ...
Так-как все названия классов и структур имеют чётко описанные правила наименования, в работе их использовать в качестве типов данных будет излишне и очень объёмно по размеру кода, что может приводить к неудобствам разработке и сложности понимания существующего кода, по этому необходимо за ранее создавать удобный тип данных для комфортной и понятной работы, с помощью модификатора typedef. Например:
class UserData {
int32_t _id;
public:
std::string name;
UserData(const int32_t id) : _id(id) {}
};
int32_t main(){
/**
* НЕПРАВИЛЬНО! название переменной понятное и правильное,
* но оно сильно похоже на тип данных, что в дальнейшем может привести к путанице и ошибкам.
*/
UserData userData(15);
userData.name = "Ivan";
std::cout << "User Name: " << userData.name << std::endl;
return 0;
}
typedef class UserData {
int32_t _id;
public:
std::string name;
UserData(const int32_t id) : _id(id) {}
} user_data_t;
int32_t main(){
/**
* ПРАВИЛЬНО! мы не нарушили никакие правила и ошибка из-за схожих названий допущена быть не может.
* Также глазу человека проще понимать на подсознании, что user_data_t это всегда тип данных.
*/
user_data_t userData(15);
userData.name = "Ivan";
std::cout << "User Name: " << userData.name << std::endl;
return 0;
}
Названия переменных
Имена переменных (включая параметры функций) и членов данных имеют формат похожий на формат названий структур и классов, за исключением того, что первая буква названия переменной всегда строчная.
Общие имена переменных
Пример:
std::string table_name; // ПЛОХО!
std::string tableName; // ОТЛИЧНО!
Члены данных класса
- Члены приватных данных классов, как статические, так и нестатические, именуются как обычные переменные, не являющиеся членами, но с начальным подчеркиванием.
- Члены публичных данных классов, как статические, так и не статические, именуются также как и обычные переменные в коде реализации.
class TableInfo {
...
private:
// OK - нижнее подчёркивание в начале
std::string _tableName;
// OK.
static Pool <TableInfo> * _pool;
};
Структура данных членов
Члены данных структур, как статических, так и нестатических, именуются как обычные переменные, не являющиеся членами. Они не имеют начальных подчеркиваний, которые есть у членов данных в классах.
struct UrlTableProperties {
std::string name;
int32_t numEntries;
static Pool <UrlTableProperties> * pool;
};
Названия констант
Переменные, объявленные как constexpr или const, и чье значение фиксировано на протяжении программы, именуются с лидирующей “k”, за которой следует смешанный регистр. Подчеркивания могут использоваться в качестве разделителей в редких случаях, когда заглавные буквы не могут быть использованы для разделения. Например:
const int32_t kDaysInAWeek = 7;
const int32_t kAndroid8_0_0 = 24; // Android 8.0.0
Все такие переменные со статической длительностью хранения (т.е. статические и глобальные, см. подробности в разделе Длительность хранения) должны именоваться таким образом, включая те, что находятся в шаблонах, где различные экземпляры шаблона могут иметь разные значения. Это соглашение необязательно для переменных других классов хранения, например, автоматических переменных; в противном случае применяются обычные правила именования переменных. Например:
void ComputeFoo(absl::string_view suffix) {
// Любой из этих вариантов приемлем.
const absl::string_view kPrefix = "prefix";
const absl::string_view prefix = "prefix";
...
}
void ComputeFoo(absl::string_view suffix) {
// Плохо — разные вызовы ComputeFoo дают kCombined разные значения.
const std::string kCombined = absl::StrCat(kPrefix, suffix);
...
}
Названия функций
Регулярные функции имеют смешанный регистр; аксессоры и мутаторы могут именоваться как переменные.
Обычно функции должны начинаться с строчной буквы и иметь заглавную букву для каждого нового слова.
addTableEntry()
deleteUrl()
openFileOrDie()
(То же правило именования применяется к константам области действия класса и пространства имён, которые представлены как часть API и которые должны выглядеть как функции, поскольку тот факт, что они являются объектами, а не функциями, является неважной деталью реализации.)
Аксессоры и мутаторы (функции get и set) могут быть названы как переменные. Они часто соответствуют фактическим переменным-членам, но это не обязательно. Например, int32_t count() и void setSount(const int32_t count).
Названия пространства имён
Названия пространств имён все строчные, слова разделены подчеркиваниями. Названия пространств имён верхнего уровня основаны на имени проекта. Избегайте конфликтов между вложенными пространствами имён и общеизвестными пространствами имён верхнего уровня.
Название пространства имён верхнего уровня обычно должно быть именем проекта или команды, код которой содержится в этом пространстве имён. Код в этом пространстве имён обычно должен находиться в каталоге, базовое имя которого совпадает с именем пространства имён (или в его подкаталогах).
Помните, что правило против сокращенных имён применяется к пространствам имён так же, как и к именам переменных. Код внутри пространства имён редко нуждается в упоминании имени пространства имён, поэтому обычно в сокращении нет особой необходимости.
Избегайте вложенных пространств имён, которые соответствуют общеизвестным пространствам имён верхнего уровня. Конфликты между именами пространств имён могут привести к неожиданным сбоям сборки из-за правил поиска имён. В частности, не создавайте никаких вложенных пространств имён std. Предпочитайте уникальные идентификаторы проектов (websearch::index, websearch::indexUtil) именам, подверженным коллизиям, таким как websearch::util. Также избегайте слишком глубоко вложенных пространств имён.
Для внутренних пространств имён будьте осторожны с добавлением другого кода в то же внутреннее пространство имён, что может привести к коллизии (внутренние помощники в команде, как правило, связаны и могут привести к коллизиям). В такой ситуации полезно использовать имя файла для создания уникального внутреннего имени (websearch::index::frobberInternal для использования в frobber.hpp).
Названия перечислителей
Перечислители (как для перечислений с областью действия, так и для перечислений без области действия) должны именоваться как макросы. То есть, используйте ENUM_NAME, а не enumName.
// ОТЛИЧНО! Супер!
enum class url_table_error_t : uint8_t {
OK = 0x00,
OUT_OF_MEMORY,
MALFORMED_INPUT,
};
// ПЛОХО и всё не правильно!
enum class UrlTableError {
oK = 0,
outOfMemory,
malformedInput,
};
До января 2009 года стиль заключался в том, чтобы называть значения enum как макросы. В Google считают, что это вызывало проблемы с конфликтами имён между значениями enum и макросами. Поэтому было репокмендованно, чтобы предпочесть именование в стиле констант. Только этим, они сами противоречат своим же правилам, изначально требуя избавиться от макросов в проекте и установкой точных правил по формированию названий макросов. Если брать за пример, даже кроссплатформенный код, работающий одновременно в 4-х основных операционных системах MS Windows, Linux, MacOS X и FreeBSD, то даже в очень крупных проектах с миллионами строк кода, никаких проблем в этом нет.
Названия макросов
Вы ведь не собираетесь определять макрос, не так ли? Если вы это сделаете, то они будут такими: MY_MACRO_THAT_SCARES_SMALL_CHILDREN_AND_ADULTS_ALIKE.
Пожалуйста, ознакомьтесь с описанием макросов; в общем случае макросы не следует использовать. Однако, если они абсолютно необходимы, то их следует называть заглавными буквами и подчеркиваниями, а также с префиксом, специфичным для проекта.
#define MYPROJECT_ROUND(x) ...
Исключения из правил именования
Если вы даёте имя чему-то, что аналогично существующей сущности C или C++, то вы можете следовать существующей схеме соглашений об именовании.
- bigopen() - имя функции, следует форме open()
- uint - typedef
- bigpos - структура или класс, следует форме pos
- sparse_hash_map - STL-подобная сущность; следует соглашениям об именовании STL
- LONGLONG_MAX - константа, как в INT_MAX
Комментарии
Комментарии абсолютно необходимы для того, чтобы наш код был читаемым. Следующие правила описывают, что и где следует комментировать. Но помните: хотя комментарии очень важны, лучший код — самодокументируемый. Давать разумные имена типам и переменным гораздо лучше, чем использовать непонятные имена, которые затем нужно объяснять в комментариях.
Когда вы пишете комментарии, пишите для своей аудитории: следующего участника, которому нужно будет понять ваш код. Будьте щедры — следующим можете оказаться вы!
Частая ошибка разработчиков С++, работая в Российской компании, писать комментарии на английском языке, возникает всегда вопрос, для кого и для чего это делается? Комментарии на английском языке абсолютно необходимы в международных проектах и в OpenSource который распространяется на весь мир, в локальных проектах комментарии просто обязаны быть на национальных языках, в противном случае этот нерадивый разработчик пособник западных спецслужб, упрощающих работу для них, а не для своей команды.
Стиль комментариев
Используйте либо синтаксис //, либо /* */, если вы последовательны.
Вы можете использовать либо синтаксис //, либо /* */; однако, // гораздо более распространен. Будьте последовательны в том, как вы комментируете и какой стиль вы используете где.
Более правильно использовать комментарии // когда текст в нём не длинный и умещается в одну строку, если текст более объемный то логичнее разделить на комментарий вида /* */.
Мы не призываем писать комментарии к каждой строчке кода, но каждая переменная член класса или структуры, должа иметь свой комментарий с описанием, что это и для чего нужно. Также блоки кода реализации тоже должны иметь своё объяснение, если код понятен и очевиден для вас лично, то он не очевиден для остальных участников которые работают над проектом. Код который не имеет вообще ни одного комментария, не допускается и считается мусором.
Комментарии файла
Запустите каждый файл с лицензионной паттерной пластиной.
Если исходный файл (например, файл .hpp) объявляет несколько абстракций, связанных с пользователем (общие функции, связанные классы и т.д.), Включите комментарий, описывающий коллекцию этих абстракций. Включите достаточно деталей для будущих авторов, чтобы знать, что там нет. Тем не менее, подробная документация об отдельных абстракциях принадлежит этим абстракциям, а не на уровне файла.
Например, если вы пишете комментарий к файлу для frobber.hpp, вам не нужно включать комментарий к файлу в frobber.cpp или frobberTest.cpp. С другой стороны, если вы напишете коллекцию классов в recordatedObjects.cpp, который не имеет связанного файла заголовка, вы должны включить комментарий к файлу в recordatedObjects.cpp.
Юридическое уведомление и строка автора
Каждый файл должен содержать лицензию. Выберите подходящую пакет для лицензии, используемой проектом (например, Apache 2.0, BSD, LGPL, GPL).
Если вы внесёте значительные изменения в файл с данными автора, рассмотрите возможность удаления строки автора. Новые файлы обычно не должны содержать уведомление об авторском праве или строку автора.
Комментарии структур и классов
Каждое неочевидное классовое или структурное объявление должно иметь сопутствующий комментарий, который описывает, для чего оно и как его следует использовать.
/**
* GargantuanTableIterator Класс итератора класса GargantuanTable
*/
class GargantuanTableIterator {
...
};
Комментарии класса
Комментарий класса должен предоставить читателю достаточно информации, чтобы узнать, как и когда использовать класс, а также любые дополнительные соображения, необходимые для правильного использования класса. Документируйте предположения синхронизации, которые делает класс, если таковые имеются. Если экземпляр класса может быть доступен по нескольким потокам, следите за тем, чтобы документировать правила и инварианты, окружающие многопоточное использование.
Комментарий класса часто является хорошим местом для небольшого примера фрагмента кода, демонстрирующего простое и сфокусированное использование класса.
При достаточной отделении (например, файлы .hpp и .cpp) комментарии, описывающие использование класса, должны соблюдать его определение интерфейса; Комментарии о работе и реализации класса должны сопровождать реализацию методов класса.
Комментарии функции
Комментарии объявления описывают использование функции (когда она не является неочевидным); Комментарии по определению функции описывают операцию.
Функциональные объявления
Почти у каждого объявления функции должны быть комментарии, предшествующие ему, которые описывают, что выполняет функция и как её использовать. Эти комментарии могут быть пропущены только в том случае, если функция проста и очевидна (например, простые аксессов для очевидных свойств класса). Частные методы и функции, объявленные в файлах .cpp, не освобождаются. Комментарии функции должны быть записаны с подразумеваемой темой этой функции и должны начинаться с глагольной фразы; Например, «открывает файл», а не «открыть файл». В целом, эти комментарии не описывают, как функция выполняет свою задачу. Вместо этого это должно быть оставлено для комментариев в определении функции.
Вещи, которые следует упомянуть в комментариях в объявлении функции:
- Каковы входы и выходы. Если имена аргументов функции приведены в «Backticks», то инструменты индексации кода могут лучше представить документацию.
- Для функций члена класса: вспоминает ли объект ссылки или указатель аргументы за пределы продолжительности вызова метода. Это довольно распространено для указателей/справочных аргументов для конструкторов.
- Для каждого аргумента указателя, разрешено ли ему быть нулевым и что произойдет, если это так.
- Для каждого вывода или аргумента ввода/вывода, что происходит в любом состоянии, в котором находится аргумент. (Например, состояние прилагается или перезаписано или перезаписывается?).
- Если есть какое-либо последствия для эффективности того, как используется функция.
Правила формирования комментария Начало всегда состоит из /** без окружающих пробелов, дальше следует перенос на новую строку. Следующая строка начинается с одного пробела, звёздочки * и ещё одного пробела, после которого следует название функции или метода, далее следует пробел и описание функции или метода. Описание параметров функции состоит из начальной звёздочки * окружённой пробелами, модификатора @param названия и описания параметра. Возвращаемое значение функции начинается с звёздочки * окружённой пробелами, модификатора @return и описания возвращаемых данных функцией. Завершение блока комментариев состоит из одного пробела и закрывающих символов */.
Вот пример:
/**
* getIterator Метод извлечения итератора
* @param startWord начальное значение строки
* @return запрашиваемое значение итератора
*/
std::unique_ptr <Iterator> getIterator(absl::string_view startWord) const;
При документировании переопределения функции сосредоточьтесь на специфике самого переопределения, а не повторяя комментарий из переопределенной функции. Во многих из этих случаев переопределение не требует дополнительной документации и следовательно, никаких комментариев не требуется.
Комментируя конструкторы и деструкторы, помните, что человек, читающий ваш код документа понимал, что конструкторы делают со своими аргументами (например, если они берут на себя ответственность за указатели), и какую очистку делает деструктор. Если это тривиально, просто пропустите комментарий. Для деструкторов довольно часто имеет место не иметь комментария к заголовку.
Определения функций
Если есть что-то сложное в том, как функция выполняет свою работу, определение функции должно иметь объяснительный комментарий. Например, в комментарии определения вы можете описать любые трюки кодирования, которые вы используете, дайте обзор шагов, которые вы выполняете, или объяснить, почему вы решили реализовать функцию так, как вы, вместо того, чтобы использовать жизнеспособную альтернативу. Например, вы можете упомянуть, почему он должен приобрести замок для первой половины функции, но почему это не нужно во второй половине.
ПРИМЕЧАНИЕ, вы не должны просто повторять комментарии, данные с объявлением функции, в файле .hpp или везде. Можно кратко повторить, что выполняет эта функция, но в центре внимания комментариев должно быть то, как она это делает.
Комментарии переменных
В целом фактическое имя переменной должно быть достаточно описательным, чтобы дать хорошее представление о том, для чего используется переменная. В некоторых случаях требуется больше комментариев.
Члены данных класса
Цель каждого члена данных класса (также называемой переменной экземпляра или переменной-члена) должна быть понятна. Если есть какие-либо инварианты (специальные ценности, отношения между членами, требования к жизни), не четко выраженные типом и именем, они должны быть прокомментированы. Однако, если тип и имя достаточно (int32_t _numEvents;), комментарий не требуется.
В частности, добавьте комментарии, чтобы описать существование и значение значений стражи, таких как nullptr или -1, когда они не очевидны. Например:
private:
/**
* Используется для доступа к таблицам за пределами.
* -1 означает, что мы ещё не знаем, сколько записей есть в таблице.
*/
int32_t _numTotalEntries;
Глобальные переменные
Все глобальные переменные должны иметь комментарий, описывающий, для чего они являются, для чего они используют, и (если не понятно), зачем они должны быть глобальными. Например:
// Общее количество тестовых случаев, через которые мы проходили в этом регрессионном тесте.
const int32_t kNumTestCases = 6;
Реализация комментариев
В вашей реализации у вас должны быть комментарии в хитрых, неочевидных, интересных или важных частях вашего кода.
Пояснительные комментарии
Хитрые или сложные кодовые блоки должны иметь комментарии перед ними.
Комментарии аргументов функции
Когда значение аргумента функции является неочевидным, рассмотрим одно из следующих средств:
- Если аргумент является постоянной, и одна и та же постоянная используется в нескольких вызовах функций таким образом, что молчаливо предполагает, что они одинаковы, вы должны использовать именованную константу, чтобы сделать это ограничение явным.
- Рассмотрим изменение сигнатуры функции, чтобы заменить аргумент bool на аргумент перечисления. Это заставит аргумент оценить самоописание.
- Для функций, которые имеют несколько параметров конфигурации, рассмотрите возможность определения одного класса или структуры для удержания всех параметров, и передайте ]этот экземпляр. Такой подход имеет несколько преимуществ. Параметры ссылаются на имя на сайте вызова, что проясняет их значение. Это также уменьшает количество аргументов функции, что облегчает чтение и написание функций. В качестве дополнительного преимущества вам не нужно менять сайты вызовов, когда вы добавляете другую опцию.
- Замените большие или сложные вложенные выражения на названные переменные.
- В качестве последней среды используйте комментарии, чтобы уточнить значения аргументов на сайте вызова.
Рассмотрим следующий пример:
// Что это за аргументы?
const decimal_number_t product = calculateProduct(values, 7, false, nullptr);
против:
product_options_t options;
options.set_precision_decimals(7);
options.set_use_cache(product_options_t::kDontUseCache);
const decimal_number_t product = calculateProduct(values, options, /* completionCallback = */nullptr);
Нельзя
Не указывайте очевидное. В частности, не опишите, что делает код, если поведение не является неочевидным для читателя, который хорошо понимает C++. Вместо этого предоставьте комментарии на более высоком уровне, которые описывают, почему код делает то, что он делает, или сделайте код самостоятельным.
Сравните это:
// Найдём элемент в векторе. <- Плохо: Очевидно!
if(std::find(v.begin(), v.end(), element) != v.end()){
Process(element);
}
К этому:
// Передаём в Process «element», если он не был обработан.
if(std::find(v.begin(), v.end(), element) != v.end()){
Process(element);
}
Код самоописания не нуждается в комментарии. Комментарий из примера выше был бы очевиден:
if(!IsAlreadyProcessed(element)){
Process(element);
}
Пунктуация, орфография и грамматика
Обратите внимание на пунктуацию, правописание и грамматику; Легче читать хорошо написанные комментарии, чем плохо написанные.
Комментарии должны быть столь же читаемыми, как и повествовательный текст, с надлежащей капитализацией и пунктуацией. Во многих случаях полные предложения являются более читаемыми, чем фрагменты предложения. Более короткие комментарии, такие как комментарии в конце строки кода, иногда могут быть менее формальными, но вы должны соответствовать вашему стилю.
Хотя может быть разочаровывающим, если рецензент кода указывает на то, что вы используете запятую, когда вам следует использовать полуколон, очень важно, чтобы исходный код поддерживал высокий уровень ясности и читаемости. Правильная пунктуация, правописание и грамматика помогают с этой целью.
Комментарии TODO
Используйте комментарии к временному коду, краткосрочное решение или хорошее, но не идеальное.
TODOS должен включать строку TODO во все ограничения, за которыми следуют идентификатор ошибки, имя, адрес электронной почты или другой идентификатор человека или проблемы с наилучшим контекстом о проблеме, на которую ссылается TODO.
// TODO: bug 12345678 - Удалите это после истечения окна совместимости 2047Q4.
// TODO: example.com/my-design-doc - Вручную исправьте этот код в следующий раз, когда он будет затронут.
// TODO(bug 12345678): Обновите этот список после того, как служба Foo будет отказана.
// TODO(John): Используйте здесь «\*» для оператора конкатенации.
Если ваш TODO имеет форму «в будущем, сделайте что-нибудь», убедитесь, что вы либо включили очень конкретную дату («Исправить к ноябрю 2005 года») или очень конкретное событие («Удалить этот код, когда все клиенты смогут обрабатывать ответы XML».).
Форматирование
Стиль кодирования и форматирование довольно произвольные, но проекту гораздо легче следовать, если все используют один и тот же стиль. Люди могут не согласиться с каждым аспектом правил форматирования, и некоторые правила могут вызвать привычку к тому, чтобы привыкать, но важно, чтобы все участники проекта следовали правилам стиля, чтобы все они могли легко читать и понимать код каждого.
Длина строки
Каждая строка текста в вашем коде должна длиться не более 80 символов.
Мы признаем, что это правило является спорным, но так много существующего кода уже придерживается этого, и мы считаем, что согласованность важна.
Плюсы:
Те, кто предпочитают это правило, утверждают, что грубо заставлять их изменять размер их окна, и нет необходимости в чем-то более. Некоторые люди привыкли иметь несколько кодовых окон Люди создают свою рабочую среду, предполагая определенную максимальную ширину окна, и 80 столбцов были традиционным стандартом. Зачем это менять?
Минусы:
Сторонники изменения утверждают, что более широкая строка может сделать код более читабельным. Лимит с 80 колоннами-это возврат к мэйнфреймах 1960-х годов; Современное оборудование имеет широкие экраны, которые могут легко показывать более длинные строки.
Решение:
80 символов - максимум.
Строка может превышать 80 символов, если это
- Строка комментариев, которая невозможно разделить, не нанося ущерб читабельности, простоте разреза и вставки или например автоматическому связующему, если строка содержит пример команду или буквальный URL более 80 символов.
- Буквальная строка, который не может быть легко заверен в 80 столбцах. Это может быть связано с тем, что он содержит URI или другие семантически критические произведения, или потому, что буквальный содержит встроенный язык или многослойный буквальный лист, новеньши, новеньши, такие как сообщения справки. В этих случаях разрыв буквальности уменьшит читабельность, способность поиска, возможность щелкнуть ссылки и т.д. За исключением кода тестирования, такие литералы должны появиться в сфере пространства имён рядом с верхней частью файла. Если такой инструмент, как Clang-Format, не распознает неуместный контент, отключите инструмент вокруг контента по мере необходимости.
(Мы должны балансировать между удобством для использования/поиска таких литералов и читаемости кода вокруг них.)
- Включение заявления.
- Использование декларации.
Не ASCII-символы
Символы, не относящиеся к ASCII, должны быть редкими и должны использовать форматирование UTF-8.
Вы не должны жестко кодировать текст пользователя в источнике, даже на английском языке, поэтому использование не ASCII-символов должно быть редким. Однако в некоторых случаях целесообразно включить такие слова в ваш код. Например, если ваш код анализирует файлы данных из иностранных источников, может быть целесообразно для жесткой кодирования строк(и) не ASCII, используемых в этих файлах данных в качестве делимитеров. Чаще всего код единиц (который не нуждается в локализовании) может содержать строки, не относящиеся к ASCII. В таких случаях вы должны использовать UTF-8, так как это кодирование, понятное большинством инструментов, способных обрабатывать больше, чем просто ASCII.
Кодирование шестигранника также в порядке и поощряется, где оно улучшает читаемость-например, «\xEF\xBB\xBF», или, даже проще, «\uFEFF» - это символ пространственного пространства Unicode Zero-Break, который мог бы быть невидимым, если включен в источник как прямой UTF-8.
Когда это возможно, избегайте префикса u8. Он имеет значительно другую семантику, начиная с C++20, чем в C++17, создавая массивы char8_t, а не char, и снова изменится в C++23.
Вы не должны использовать типы символов char16_t и char32_t, так как они для текста без UTF-8. По аналогичным причинам вам также не следует использовать wchar_t (если вы не пишите код, который взаимодействует с API Windows, который широко использует wchar_t).
Табуляция или Пробелы
Используйте только табуляцию (символ \t) и размером 4.
Мы используем табуляцию для выравнивания. Не используйте пробелы в вашем коде. Вы должны установить свой редактор для настроек табуляции, когда вы нажимаете на клавишу TAB.
*Преимущества табуляции перед пробелами:
- Гибкость настройки. Каждый программист может настроить длину табуляции под свой вкус. Например, для кода с большой вложенностью можно поставить ширину табуляции в два пробела, а для другого — в четыре.
- Удобство работы с посторонними библиотеками. Использование табуляции не накладывает ограничение на стиль, в то время как поддержка одновременно нескольких библиотек с разным размером пробело-табуляции может быть проблематичной.
- Оптимизация размера файла. Табуляция занимает меньше памяти, чем использование, например, 5 пробелов.
- Если объём кода большой то разработка превращается в ад, попробуйте выровнять большой кусок кода, когда только чтобы удалить лишние смещения требуется тысячу раз нажать клавишу клавиатуры BACK SPACE.
- То что современные IDE хорошо работают с пробелами имитируя работу с табуляцией, не больше чем миф. Мне приходилось анализировать много кода из разных IDE и везде были проблемы с выравниванием, невозможно правильно выровнять код пробелами, всегда будут лишние отступы.
Функциональные объявления и определения
Верните тип на той же строке, что и имя функции, параметры на той же строке, если они подходят. Списки параметров обертывания, которые не подходят на одну строку, так как вы обернёте аргументы в функциональный вызов.
Функции выглядят так:
ReturnType ClassName::FunctionName(const type_t parName1, const type_t parName2){
DoSomething();
...
}
Если у вас слишком много текста, чтобы поместиться на одной строке:
ReturnType ClassName::ReallyLongFunctionName(
const type_t parName1,
const type_t parName2,
const type_t parName3
){
DoSomething();
...
}
или если вы не можете установить даже первый параметр:
ReturnType LongClassName::ReallyReallyReallyLongFunctionName(
const type_t parName1,
const type_t parName2,
const type_t parName3
){
DoSomething();
...
}
Некоторые моменты, чтобы отметить:
- Выберите хорошие имена параметров.
- Имя параметра может быть пропущено, только если параметр не используется в определении функции.
- Если вы не можете установить тип возврата и имя функции на одной строке, разложите между ними.
- Если что-то сломается после возврата типа объявления или определения функции, не отступайте.
- Открытая скобка всегда находится на одной строке, что и имя функции.
- Между именем функции и открытым скобком никогда не бывает места.
- Между скобками и параметрами никогда не бывает места.
- Открытая фигурная скобка всегда находится на конце последней строки объявления функции, а не начало следующей строки.
- Закрывающая фигурная скобка находится либо на последней строке, либо на той же линии, что и открытая фигурная скобка.
- Должно быть пространство между ближними скобками и открытой фигурной скобкой.
- Все параметры должны быть выровнены, если это возможно.
Неиспользованные параметры, которые очевидны из контекста, могут быть опущены:
class Foo {
public:
Foo(const Foo &) = delete;
Foo & operator = (const Foo &) = delete;
};
Неиспользуемые параметры, которые могут быть не очевидны, чтобы прокомментировать имя переменной в определении функции:
class Shape {
public:
virtual void rotate(const double radians) = 0;
};
class Circle : public Shape {
public:
void rotate(const double radians) override;
};
void Circle::rotate(const double /* radians */) {}
// Плохо - если кто-то хочет реализовать позже, неясно, что означает переменная.
void Circle::rotate(const double) {}
Атрибуты и макросы, которые расширяются до атрибутов, появляются в самом начале объявления или определения функции, перед типом возврата:
ABSL_ATTRIBUTE_NOINLINE void expensiveFunction();
[[nodiscard]] bool IsOk();
Встраиваемые выражения
Параметры и тела формата, как для любой другой функции, и списков захвата, таких как другие списки, разделённые запятыми.
Для пострадавших отлова не оставляйте пространство между амперсандом (&) и именем переменной.
int32_t x = 0;
auto xPlusN = [&x](const int32_t n) -> int32_t {
return (x + n);
}
Короткая лямбда могет быть написана в строке в качестве аргументов функций.
absl::flat_hash_set <int32_t> to_remove = {7, 8, 9};
std::vector <int32_t> digits = {3, 9, 1, 8, 4, 7, 1};
digits.erase(std::remove_if(digits.begin(), digits.end(), [&to_remove](int32_t i){
return to_remove.contains(i);
}), digits.end());
Литералы с плавающей точкой
Литералы с плавающей запятой всегда должны иметь radix point, с цифрами с обеих сторон, даже если они используют экспоненциальную нотацию. Читаемость улучшается, если все литералы с плавающей запятой принимают эту знакомую форму, поскольку это помогает убедиться, что они не выдаются за целочисленные литералы, и что E/e экспоненциальной нотации не выдаётся за шестнадцатеричную цифру. Хорошо инициализировать переменную с плавающей точкой с целочисленным литералом (при условии, что тип переменной может точно представлять это целое число), но обратите внимание, что число в экспоненциальной нотации никогда не является буквально целочисленным.
float f = 1.f;
long double ld = -.5L;
double d = 1248e6;
float f = 1.0f;
float f2 = 1.0; // Очень хорошо
float f3 = 1; // Очень хорошо
long double ld = -0.5L;
double d = 1248.0e6;
Функциональные вызовы
Либо запишите вызов в одну строку, оберните аргументы в скобки, либо запустите аргументы на новой линии, отступаемая в четырех местах и продолжайте в этом 4 пробела. В отсутствие других соображений используйте минимальное количество строк, включая размещение нескольких аргументов на каждую строку, где это необходимо.
Вызовы функций имеют следующий формат:
bool result = doSomething(argument1, argument2, argument3);
Если аргументы не все вписываются в одну линию, они должны быть разбиты на несколько строк, причем каждая последующая строка выровняется с первым аргументом. Не добавляйте места после открытой пары или перед закрытием парены:
bool result = doSomething(averyveryveryverylongargument1,
argument2, argument3);
Аргументы могут быть при желании размещены на последующих строках с четырьмя пробелами:
if(...){
...
...
if(...){
bool result = doSomething(
argument1, argument2,
argument3, argument4
);
...
}
Поместите несколько аргументов на одну строку, чтобы уменьшить количество строк, необходимых для вызова функции, если нет конкретной проблемы читаемости. Некоторые считают, что форматирование со строго одним аргументом на каждой строке является более читабельным и упрощает редактирование аргументов. Тем не менее, мы расставляем приоритеты для читателя по поводу простоты редактирования аргументов, и большинство проблем с читаемости лучше решаются с помощью следующих методов.
Если наличие нескольких аргументов в одной строке уменьшает читаемость из-за сложности или запутанного характера выражений, которые составляют некоторые аргументы, попробуйте создать переменные, которые отражают эти аргументы в отношении описательного имени:
int32_t myHeuristic = (scores[x] * y + bases[x]);
bool result = doSomething(myHeuristic, x, y, z);
Или поместите запутанный аргумент на свою линию с объяснительным комментарием:
bool result = doSomething(scores[x] * y + bases[x], // Оценка эвристики.
x, y, z);
Если все ещё есть случай, когда один аргумент значительно более читабелен на собственной строке, то поместите его на свою строку. Решение должно быть специфичным для аргумента, который станет более читабельным, а не общей политикой.
Иногда аргументы образуют структуру, которая важна для читаемости. В этих случаях не стесняйтесь форматировать аргументы в соответствии с этой структурой:
// Преобразовать виджет матрицей 3x3.
myWidget.Transform(x1, x2, x3,
y1, y2, y3,
z1, z2, z3);
Приготовленная инициализация списка формата
Форматируйте список инициализаторов, точно так же, как вы бы отформатировали функциональный вызов на его месте.
Если список привязанного к имени (например, тип или имя переменной), формат, как если бы {} были скобками вызова функции с этим именем. Если имя нет, предположим, что имя нулевой длины.
// Примеры списка Bred Init на одной строке.
return {foo, bar};
functioncall({foo, bar});
std::pair <int32_t, int32_t> p{foo, bar};
// Когда вам нужно обернуть.
someFunction(
{"assume a zero-length name before {"},
some_other_function_parameter);
some_type_t 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}};
some_type_t variable{
"This is too long to fit all in one line"};
my_type_t m = { // Здесь вы также можете сломаться до {.
superlongvariablename1,
superlongvariablename2,
{short, interior, list},
{interiorwrappinglist,
interiorwrappinglist2}};
Записки по циклу и разветвлению
На высоком уровне операторы цикла или ветвления состоят из следующих компонентов:
- Одно или несколько ключевых слов оператора (например: if, else, switch, while, do, или for).
- Одно условие или спецификатор итерации, внутри скобок.
- Одно или несколько контролируемых операторов или блоков контролируемых операторов.
Для этих заявлений:
- Компоненты утверждения не должны быть разделены отдельными пробелами.
- Внутри условия или спецификатора итерации поместите один пробел (или разрыв строки) между каждым полуколоном и следующим токеном, за исключением случаев, когда токен является заключительной скобкой или другим полуколоном.
- Внутри условия или спецификатора итерации не ставят пробел после открытия скобки или перед закрытием скобки.
- Поместите любые контролируемые операторы внутри блоков (то есть используйте фигурные скобки).
- Внутри контролируемых блоков положите одну строку сразу же после открывающейся скобки, и одна строка разрывается непосредственно перед закрывающей скобкой.
- Если в реализации условия или цикла, всего одна строчка кода, в фигурные скобки такой код можно не оборачивать.
- Если в условии есть дополнительного условие идущее дальше, один пробел ставится между закрывающей фигурной скобкой предыдущего условия и модификатора esle далее следует ещё один пробел потом условие if, а вот в данном случае пробел должен ставиться между закрывающей круглой скобкой и октрывающей фигурной.
// Хорошо - никаких пробелов внутри скобок, и пробелов перед скобкой.
if(condition){
doOneThing();
doAnotherThing();
/**
* Хорошо - пробел стоит между закрывающей фигурной скобкой предыдущего условия и модификатором else
* а также после закрывающей круглой скобкой и открывающей фигурной.
*/
} else if(int32_t a = f(); a != 3) {
doOneThing();
doAThirdThing(a);
// Хорошо - фигурные скобки не нужны так-как реализация состоит из одной строки
} else doNothing();
// Хорошо - фигурные скобки здесь не нужны, так-как в цикле всего одна строка реализации
while(condition)
repeatAThing();
/**
* Хорошо - между модификатором while и открывающей круглой скобкой пробела нет,
* между закрывающей круглой скобкой и открывающей фигурной скобкой тоже пробела нет.
*/
while(condition){
bool condition2 = condition;
repeatAThing(condition2);
}
// Хорошо - между модификатором while и открытой круглой скобкой пробела нет
do {
repeatAThing();
} while(condition);
// Хорошо - фигурные скобки здесь не нужны, так-как в цикле всего одна строка реализации
for(int i = 0; i < 10; ++i)
repeatAThing();
if(condition) {} // Плохо - лишний пробел между закрывающей круглой скобкой и открывающей фигурной
else if ( condition ) {} // Плохо - много лишних пробелов
else if (condition){} // Плохо - между условием if и открывающей круглой скобкой есть пробел
else if(condition){} // Плохо - так-как это дополнительное условие, между закрывающей круглой скобкой и открывающей фигурной скобкой, должен быть пробел
for (int32_t a = f();a == 10) {} // Плохо - пропущены пробелы между параметрами и лишний пробел между модификатором for и открывающейся круглой скобкой и лишний пробел между закрывающей круглой скобкой и открывающей фигурной
// Плохо, между модификатором if и открывающей круглой скобкой есть пробел, а после модификатора else стоят фигурные скобки, а они здесь не нужны, реализация однострочная.
if (condition)
foo;
else {
bar;
}
// Плохо, между модификатором if и открывающей круглой скобкой есть пробел
if (condition)
// Комментарий
doSomething();
// Плохо, между модификатором if и открывающей круглой скобкой есть пробел и ненужный перенос на новую строку второго аргумента условия, тем самым ухудшающего чтение.
if (condition1 &&
condition2)
doSomething();
// Хорошо - подходит на одну линию.
if(x == kFoo){ return new Foo(); }
// Хорошо - в этом случае скобки являются необязательными.
if(x == kFoo)
return new Foo();
// Хорошо - Состояние подходит на одну линию, тело подходит на другую.
if(x == kBar)
Bar(arg1, arg2, arg3);
Это исключение не применяется к операторам с несколькими ключами слов, как if … else, или do … while.
// Плохо - `if ... else` скобки отсутствует и лишние пробелы.
if (x) DoThis();
else DoThat();
// Плохо - `do ... while` утверждение отсутствует и лишние пробелы.
do DoThis();
while (x);
Используйте этот стиль только тогда, когда оператор является кратким, и учитывайте, что циклы и операторы ветвления со сложными условиями или контролируемыми операторами могут быть более читаемыми с фигурными скобками. Некоторые проекты всегда требуют фигурных скобок.
Блоки корпусов в операторах switch могут иметь фигурные скобки или нет, в зависимости от ваших предпочтений. Если вы включите фигурные скобки, их следует размещать, как показано ниже.
// Хорошо - нет лишних пробелов между оператором switch и открытой круглой скобки и между закрытой круглой скобки и открытой фигурной скобки.
switch(var){
case 0: {
Foo();
} break;
default: {
Bar();
}
}
Пустые тела циклов должны использовать либо пустую пару переносов, либо продолжаться без переносов, а не одного полуколона.
// Отлично - фигурные скобки не нужны.
while(condition);
while(condition){
// Комментарии чего-то там далее...
}
while(condition)
continue; // Отлично - `continue` вполне логично.
// Плохо - не имеет смысла, фигурные скобки никакой логики не несут, зато занимают место.
while(condition){}
Указатель и ссылка выражения
Нет пробелов вокруг периода или стрелки. Операторы указателей не имеют пробелов.
Ниже приведены примеры правильно формированного указателя и эталонных выражений:
x = *p;
p = &x;
x = r.y;
x = r->y;
Обратите внимание, что:
- При доступе к участнику нет мест или стрелки.
- Операторы указателей не имеют пробелов после * или &.
При ссылке на указатель или ссылку (объявления или определения переменных, аргументы, типы возврата, параметры шаблона и т.д.) Вы можете разместить пробелы до или после звездочки/амперсанд. В стиле Trainling Space пробелы в некоторых случаях элиментируется (параметры шаблона и т.д.).
// Это хорошо, пробел предшествует.
char * c;
const std::string & str;
int32_t * getPointer();
std::vector <char *>
// Это нормально, пробел следующий (или поэлизовано).
char * c;
const std::string & str;
int32_t * getPointer();
std::vector <char *> // Обратите внимание '*' и '>'
Вы должны делать это последовательно в одном файле. При изменении существующего файла используйте стиль в этом файле.
Разрешено (если необычно) объявлять несколько переменных в одном и том же объявлении, но оно запрещено, если какие-либо из них имеют указатель или эталонные украшения. Такие записи легко неправильно прочитать.
// Хорошо, если полезно для читаемости.
int32_t x, y;
int x, *y; // Запрещено - нет & или * в нескольких объявлениях
int* x, *y; // Запрещено - нет & или * в нескольких объявлениях; противоречивое расстояния
char * c; // Отлично - * разделена пробелами, такие конструкции очень хорошо читаются
const std::string & str; // Отлично - & разделена пробелами, такие конструкции очень хорошо читаются
Логические выражения
Если у вас есть логическое выражение, длина которого превышает стандартную длину строки, будьте последовательны в том, как вы разбиваете строки.
В этом примере логический оператор AND всегда находится в конце строк:
if(thisOneThing > thisOtherThing &&
aThirdThing == aFourthThing &&
yetAnother && lastOne){
...
}
Обратите внимание, что когда код переносится в этом примере, оба логических оператора && логического И, находятся в конце строки. Это более распространено в коде Google, хотя перенос всех операторов в начало строки также допускается. Не стесняйтесь вставлять дополнительные скобки разумно, поскольку они могут быть очень полезны для повышения читабельности при правильном использовании, но будьте осторожны с чрезмерным использованием. Также обратите внимание, что вы всегда должны использовать операторы пунктуации, такие как && и ~, а не операторы слов, такие как and и compl.
Возвращаемые значения
Не окружайте выражение return скобками без необходимости.
Используйте скобки в return expr; только там, где вы бы использовали их в x = expr;.
// В простом случае скобки отсутствуют.
return result;
// Скобки позволяют сделать сложное выражение более читабельным.
return (someLongCondition && anotherCondition);
return (value); // Вы не будете писать var = (value);
return(result); // return — это не функция!
Инициализация переменных и массивов
Вы можете выбрать между =, () и {}; все следующие варианты верны:
int32_t x = 3;
int32_t x(3);
int32_t x{3};
std::string name = "Some Name";
std::string name("Some Name");
std::string name{"Some Name"};
Будьте осторожны при использовании списка инициализации в фигурных скобках для типа с конструктором std::initializer_list. Непустой braced-init-list предпочитает конструктор std::initializer_list, когда это возможно. Обратите внимание, что пустые фигурные скобки {} являются специальными и вызовут конструктор по умолчанию, если он доступен. Чтобы принудительно использовать конструктор, не являющийся std::initializer_list, используйте круглые скобки вместо фигурных скобок.
std::vector <int32_t> v(100, 1); // Вектор, содержащий 100 элементов: Все единицы.
std::vector <int32_t> v{100, 1}; // Вектор, содержащий 2 элемента: 100 и 1.
Также фигурная скобка предотвращает сужение целочисленных типов. Это может предотвратить некоторые типы ошибок программирования.
int32_t pi(3.14); // OK -- pi == 3.
int32_t pi{3.14}; // Ошибка компиляции: сужающее преобразование.
Директивы препроцессора
В Google считают, что знак решетки, который начинает директиву препроцессора, всегда должен находиться в начале строки.
Даже если директивы препроцессора находятся внутри тела отступаемого кода, директивы должны начинаться в начале строки.
Неправильный подход:
if (lopsided_score) {
#if DISASTER_PENDING
DropEverything();
# if NOTIFY
NotifyClient();
# endif
#endif
BackToNormal();
}
Мы считаем обратное, кроме как ад перфекциониста данный подход назвать нельзя, мало того, что становится абсолютно не понятна вложенность таких директив, если кода будет много, концов вообще будет не найти, отлаживать такой код будет невозможно.
Правильный подход:
if(lopsided_score){
#if DISASTER_PENDING
dropEverything();
#endif
backToNormal();
}
Формат класса
Разделы в публичном, защищенном и частном порядке, каждый с отступом в один пробел.
Все методы и переменные текущего объекта должны всегда вызываться только через модификатор this->, так в большом коде, будет очевидно, что вызываемые методы и переменные относятся к этому объекту и для их поиск не составит много времени. К сожалению многие принебрегают этим правилом и тем самым превращая свой код в мусор, где всплывают фантомные переменные которые найти в большом проекте становится невозможно.
Базовый формат определения класса (без комментариев, см. Комментарии к классу для обсуждения того, какие комментарии необходимы):
class MyClass : public OtherClass {
private:
int32_t _someVar;
int32_t _someOtherVar;
private:
bool someInternalFunction();
public:
void setSomeVar(const int32_t var){
this->_someVar = var;
}
int32_t someVar() const {
return this->_someOtherVar;
}
public:
void someFunction();
void someFunctionThatDoesNothing(){}
public:
MyClass();
explicit MyClass(const int32_t var);
public:
~MyClass(){}
};
На что следует обратить внимание:
- Любое имя базового класса должно быть на той же строке, что и имя подкласса, с учетом ограничения в 80 столбцов.
- Ключевые слова public:, protected: и private: должны быть с отступом в один таб.
- За исключением первого случая, этим ключевым словам должна предшествовать пустая строка. Это правило необязательно для небольших классов.
- Не оставляйте пустую строку после этих ключевых слов.
- Раздел private: должен быть первым, за ним должен следовать раздел protected: и, наконец, раздел public:.
- Правила упорядочивания объявлений в каждом из этих разделов см. в разделе «Порядок объявлений».
Списки инициализаторов конструктора
Списки инициализаторов конструктора могут быть все на одной строке или с последующими строками с отступом в один таб.
Допустимые форматы для списков инициализаторов:
// Когда всё умещается в одну строку:
MyClass::MyClass(const int32_t var) : some_var_(var) {
this->doSomething();
}
/**
* Если сигнатура и список инициализаторов не находятся на одной строке,
* необходимо сделать перенос перед двоеточием и отступ в 1 пробел:
*/
MyClass::MyClass(const int32_t var) :
_someVar(var), _someOtherVar(var + 1) {
this->doSomething();
}
// Если список занимает несколько строк, поместите каждый элемент на отдельной строке и выровняйте их:
MyClass::MyClass(const int32_t var) :
// 1 пробел отступа
_someVar(var),
_someOtherVar(var + 1) {
this->doSomething();
}
/**
* Как и в случае с любым другим блоком кода, закрывающая фигурная скобка может находиться на той же строке,
* что и открывающая фигурная скобка, если она там помещается.
*/
MyClass::MyClass(const int32_t var) :
_someVar(var) {}
Форматирование пространства имён
Опять таки, в Google считают, что содержимое пространств имён не имеет отступа.
Пространства имён не добавляют дополнительный уровень отступа. Например, используйте:
namespace {
void foo(){ // Никаких дополнительных отступов внутри пространства имен.
...
}
} // namespace
Зачем в Google продвигают этот бред, понять не возможно, в случе огромного количества строк кода, никаких концов найти будет не возможно и определить какой блок кода от чего зависит тоже, особенно если будут вложенные другие пространства имён. Абсолютно не читабельный и вредный подход.
Правильное решение:
namespace {
// Отлично! - всё четко и понятно
void foo(){
...
}
} // namespace
Горизонтальные пробелы
Использование горизонтальных пробелов зависит от местоположения. Никогда не ставьте завершающие пробелы в конце строки.
Общее
int32_t i = 0; // Один пробел перед комментариями в конце строки.
void f(const bool b){ // Перед открытыми скобками никаких пробелов быть не должно.
...
int32_t i = 0; // Перед точкой с запятой обычно не ставится пробел.
// Пробелы внутри фигурных скобок для braced-init-list необязательны. Если вы их используете, ставьте их с обеих сторон!
int32_t x[] = { 0 };
int32_t x[] = {0};
// Пробелы вокруг двоеточия в списках наследования и инициализации.
class Foo : public Bar {
public:
void reset(){ baz_ = 0; } // Пробелы, отделяющие фигурные скобки от реализации.
public:
// Для встроенных реализаций функций поставьте пробелы между фигурными скобками и самой реализацией.
Foo(const int32_t b) : Bar(), baz_(b) {} // Внутри пустых скобок пробелов нет.
...
Добавление завершающего пробела может вызвать дополнительную работу для других, редактирующих тот же файл, когда они объединяют его, как и удаление существующих завершающих пробелов. Итак: не вводите завершающий пробел. Удалите его, если вы уже изменяете эту строку, или сделайте это в отдельной операции очистки (предпочтительно, когда никто другой не работает с файлом).
Циклы и условные операторы
if(b){ // Нет пробелов после ключевого слова в условиях и циклах.
} else { // Пробелы вокруг else.
}
while(test) {} // Внутри скобок обычно нет пробела.
switch(i){
for(int32_t i = 0; i < 5; ++i){
// Циклы и условия могут иметь пробелы внутри скобок, но это редкость. Будьте последовательны.
switch( i ){
if( test ){
for( int32_t i = 0; i < 5; ++i ){
/**
* В циклах For после точки с запятой всегда ставится пробел.
* Перед точкой с запятой может быть пробел, но это бывает редко.
*/
for( ; i < 5 ; ++i){
...
// Циклы For на основе диапазона всегда имеют пробел до и после двоеточия.
for(auto x : counts){
...
}
switch(i){
case 1: // В случае переключения пробел перед двоеточием не ставится.
...
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;
Вертикальное пустое пространство
Минимизируйте использование вертикальных пробелов.
Это скорее принцип, чем правило: не используйте пустые строки, когда в этом нет необходимости. В частности, не размещайте между функциями больше одной или двух пустых строк, не начинайте функции с пустой строки, не заканчивайте функции пустой строкой и будьте осторожны с использованием пустых строк. Пустая строка внутри блока кода служит как разрыв абзаца в прозе: визуально разделяя две мысли.
Основной принцип: чем больше кода помещается на одном экране, тем легче отслеживать и понимать поток управления программой. Используйте пробелы целенаправленно, чтобы обеспечить разделение в этом потоке.
Некоторые практические правила, которые могут помочь, когда пустые строки могут быть полезны:
- Пустые строки в начале или конце функции не улучшают читаемость.
- Пустые строки внутри цепочки блоков if-else могут улучшить читаемость.
- Пустая строка перед строкой комментария обычно улучшает читаемость — введение нового комментария предполагает начало новой мысли, а пустая строка дает понять, что комментарий идет со следующим, а не с предыдущим.
- Пустые строки непосредственно внутри объявления пространства имён или блока пространств имён могут улучшить читаемость, визуально отделяя несущий контент от (в основном несемантической) организационной оболочки. Особенно, когда первому объявлению внутри пространства имён предшествует комментарий, это становится особым случаем предыдущего правила, помогая комментарию «прикрепиться» к последующему объявлению.
Исключения из правил
Описанные выше соглашения о кодировании являются обязательными. Однако, как и все хорошие правила, они иногда имеют исключения, которые мы обсуждаем здесь.
Существующий несоответствующий код
Вы можете отклониться от правил при работе с кодом, который не соответствует этому руководству по стилю.
Если вы обнаружите, что изменяете код, написанный по спецификациям, отличным от представленных в этом руководстве, вам, возможно, придется отклониться от этих правил, чтобы оставаться в соответствии с локальными соглашениями в этом коде. Если вы сомневаетесь, как это сделать, спросите у оригинального автора или человека, который в настоящее время отвечает за код. Помните, что согласованность включает в себя и локальную согласованность.
Код Windows
Программисты Windows разработали свой собственный набор соглашений о кодировании, в основном основанный на соглашениях в заголовках Windows и другом коде Microsoft. Мы хотим, чтобы любой мог легко понять ваш код, поэтому у нас есть единый набор рекомендаций для всех, кто пишет на C++ на любой платформе.
Стоит повторить несколько рекомендаций, которые вы можете забыть, если привыкли к распространенному стилю Windows:
- Не используйте венгерскую нотацию (например, назовите целое число iNum). Используйте соглашения об именовании нашего руководства, включая расширение .cpp для исходных файлов.
- Windows определяет множество собственных синонимов для примитивных типов, таких как DWORD, HANDLE и т.д. Вполне приемлемо и рекомендуется использовать эти типы при вызове функций Windows API. Тем не менее, держитесь как можно ближе к базовым типам C++. Например, используйте const TCHAR * вместо LPCTSTR.
- При компиляции с Microsoft Visual C++ установите для компилятора уровень предупреждений 3 или выше и обрабатывайте все предупреждения как ошибки.
- Не используйте #pragma один раз; вместо этого используйте стандартные защитные функции include этого руководства. Путь в защитных функциях include должен быть относительным к вершине дерева вашего проекта.
- На самом деле, не используйте никаких нестандартных расширений, таких как #pragma и __declspec, если только это не является абсолютно необходимым. Использование __declspec(dllimport) и __declspec(dllexport) разрешено; однако вы должны использовать их через макросы, такие как DLLIMPORT и DLLEXPORT, чтобы кто-то мог легко отключить расширения, если он поделится кодом.
Однако есть несколько правил, которые нам иногда приходится нарушать в Windows:
- Обычно мы настоятельно не рекомендуем использовать множественное наследование реализации; однако оно необходимо при использовании COM и некоторых классов ATL/WTL. Вы можете использовать множественное наследование реализации для реализации классов и интерфейсов COM или ATL/WTL.
- Хотя вам не следует использовать исключения в вашем собственном коде, они широко используются в ATL и некоторых STL, включая тот, который поставляется с Visual C++. При использовании ATL вы должны определить _ATL_NO_EXCEPTIONS для отключения исключений. Вам следует выяснить, можно ли также отключить исключения в вашем STL, но если нет, то можно включить исключения в компиляторе. (Обратите внимание, что это нужно только для того, чтобы STL компилировался. Вам по-прежнему не следует писать код обработки исключений самостоятельно.)
- Обычный способ работы с предварительно скомпилированными заголовками — включить файл заголовка в начало каждого исходного файла, обычно с именем вроде StdAfx.h или precompile.h. Чтобы упростить обмен кодом с другими проектами, избегайте явного включения этого файла (кроме precompile.cc) и используйте параметр компилятора /FI для автоматического включения файла.
- Заголовки ресурсов, которые обычно называются resource.hpp и содержат только макросы, не обязательно должны соответствовать этим рекомендациям по стилю.
Описание
Описание стиля кода C++