Руководство по стилю кода C++
Общие рекомендации
Ниже приведены рекомендации, а не требования. Если вы редактируете код, имеет смысл придерживаться форматирования существующего кода. Стиль кода нужен для обеспечения единообразия. Единообразие упрощает чтение кода, а также облегчает поиск по коду. Многие правила не имеют логического обоснования; они продиктованы устоявшейся практикой.
Форматирование
1. Большая часть форматирования выполняется автоматически с помощью clang-format.
2. Ширина отступа — 4 пробела. Настройте свою среду разработки так, чтобы нажатие клавиши Tab добавляло четыре пробела.
3. Открывающая и закрывающая фигурные скобки должны располагаться на отдельной строке.
4. Если всё тело функции — это один оператор (statement), его можно записать в одну строку. Ставьте пробелы вокруг фигурных скобок (помимо пробела в конце строки).
5. Для функций не ставьте пробелы вокруг скобок.
6. В выражениях if, for, while и других перед открывающей скобкой ставится пробел (в отличие от вызовов функций).
7. Добавляйте пробелы вокруг бинарных операторов (+, -, *, /, %, ...) и тернарного оператора ?:.
8. При вводе символа перевода строки перенесите оператор на новую строку и увеличьте перед ним отступ.
9. При необходимости вы можете использовать пробелы для выравнивания внутри строки.
10. Не ставьте пробелы вокруг операторов ., ->.
При необходимости оператор можно перенести на следующую строку. В этом случае отступ перед ним увеличивается.
11. Не используйте пробел между унарными операторами (--, ++, *, &, ...) и аргументом.
12. Ставьте пробел после запятой, но не перед ней. То же правило действует для точки с запятой внутри выражения for.
13. Не ставьте пробелы вокруг оператора [].
14. В выражении template <...> используйте пробел между template и <; не ставьте пробелы после < или перед >.
15. В классах и структурах располагайте public, private и protected на одном уровне с class/struct, а остальной код смещайте внутрь с отступом.
16. Если одно и то же namespace используется для всего файла и нет ничего иного примечательного, отступ внутри namespace не обязателен.
17. Если блок для выражения if, for, while или другого состоит из одного statement, фигурные скобки можно опустить. Вместо этого разместите statement на отдельной строке. Это правило также применяется к вложенным if, for, while, ...
Но если вложенный statement содержит фигурные скобки или else, внешний блок следует оформлять в фигурных скобках.
18. В конце строк не должно быть пробелов.
19. Исходные файлы используют кодировку UTF-8.
20. В строковых литералах можно использовать символы, выходящие за пределы ASCII.
21. Не пишите несколько выражений в одной строке.
22. Группируйте блоки кода внутри функций и разделяйте их не более чем одной пустой строкой.
23. Разделяйте функции, классы и т. д. одной или двумя пустыми строками.
24. Модификатор const (относящийся к значению) должен записываться перед именем типа.
25. При объявлении указателя или ссылки знаки * и & должны быть отделены пробелами с двух сторон.
26. При использовании шаблонных типов задавайте для них псевдонимы с помощью ключевого слова using (кроме самых простых случаев).
Иными словами, параметры шаблона задаются только в using и далее в коде не повторяются.
using может объявляться локально, например, внутри функции.
27. Не объявляйте несколько переменных разных типов в одном объявлении.
28. Не используйте приведение типов в стиле C.
29. В классах и структурах группируйте члены данных и функции‑члены отдельно в рамках каждой области видимости.
30. Для небольших классов и структур нет необходимости отделять объявление метода от его реализации.
То же относится к небольшим методам в любых классах или структурах.
Для шаблонных классов и структур не отделяйте объявления методов от реализации (поскольку в противном случае они должны быть определены в одной и той же единице трансляции).
31. Вы можете переносить строки при достижении 140 символов вместо 80.
32. Всегда используйте префиксные операторы инкремента/декремента, если постфиксный вариант не требуется.
Комментарии
1. Обязательно добавляйте комментарии ко всем нетривиальным фрагментам кода.
Это очень важно. Написание комментария может помочь вам понять, что этот участок кода не нужен или что он спроектирован неправильно.
2. Комментарии могут быть настолько подробными, насколько необходимо.
3. Размещайте комментарии перед кодом, который они описывают. В редких случаях комментарии могут располагаться после кода, на одной строке.
4. Комментарии должны быть написаны только на английском языке.
5. Если вы пишете библиотеку, добавляйте подробные комментарии, объясняющие её, в основной заголовочный файл.
6. Не добавляйте комментарии, которые не несут дополнительной информации. В частности, не оставляйте пустые комментарии вроде этого:
Пример заимствован с сайта http://home.tamk.fi/~jaalto/course/coding-style/doc/unmaintainable-code/.
7. Не пишите мусорные комментарии (автор, дата создания и т. д.) в начале каждого файла.
8. Однострочные комментарии начинаются с трёх слэшей: ///, а многострочные — с /**. Такие комментарии считаются «документацией».
Примечание: вы можете использовать Doxygen для генерации документации из этих комментариев. Но Doxygen обычно не используют, потому что удобнее просматривать код в IDE.
9. Многострочные комментарии не должны иметь пустых строк в начале и в конце (кроме строки, которая закрывает многострочный комментарий).
10. Для закомментирования кода используйте обычные комментарии, а не «документирующие» комментарии.
11. Удаляйте закомментированные части кода перед коммитом.
12. Не используйте ненормативную лексику в комментариях или коде.
13. Не пишите ЗАГЛАВНЫМИ БУКВАМИ. Не используйте избыточную пунктуацию.
14. Не используйте комментарии в качестве разделителей.
15. Не начинайте обсуждения в комментариях.
16. Нет необходимости писать в конце блока комментарий, поясняющий, чему он посвящён.
Имена
1. Используйте строчные буквы и символ нижнего подчеркивания в именах переменных и членов классов.
2. Для имен функций (методов) используйте стиль camelCase, начиная с строчной буквы.
3. Для имен классов и структур используйте CamelCase, начиная с заглавной буквы. Для интерфейсов не используются префиксы, кроме I.
4. Объявления using именуются так же, как классы.
5. Имена аргументов типовых параметров: в простых случаях используйте T; T, U; T1, T2.
В более сложных случаях либо следуйте правилам для имен классов, либо добавляйте префикс T.
6. Имена константных аргументов шаблонов: либо следуют правилам для имён переменных, либо в простых случаях используют N.
7. Для абстрактных классов (интерфейсов) можно добавлять префикс I.
8. Если вы используете переменную локально, можно использовать краткое имя.
Во всех остальных случаях используйте имя, отражающее назначение.
9. Имена define-ов и глобальных констант записываются в формате ALL_CAPS с подчёркиваниями.
10. Имена файлов должны соответствовать стилю их содержимого.
Если файл содержит единственный класс, назовите файл так же, как класс (CamelCase).
Если файл содержит единственную функцию, назовите файл так же, как функцию (camelCase).
11. Если имя содержит аббревиатуру, то:
- Для имен переменных аббревиатура должна быть записана строчными буквами:
mysql_connection(а неmySQL_connection). - Для имен классов и функций сохраняйте заглавные буквы в аббревиатуре:
MySQLConnection(а неMySqlConnection).
12. Аргументы конструктора, которые используются только для инициализации членов класса, должны именоваться так же, как члены класса, но с символом подчеркивания в конце.
Суффикс с символом нижнего подчеркивания можно опустить, если аргумент не используется в теле конструктора.
13. Не должно быть различий в именовании локальных переменных и членов класса (префиксы не требуются).
14. Для констант в enum используйте CamelCase, начиная с заглавной буквы. Допустим также вариант ALL_CAPS. Если enum является нелокальным, используйте enum class.
15. Все имена должны быть на английском языке. Транслитерация слов на иврите не допускается.
Не так: T_PAAMAYIM_NEKUDOTAYIM
16. Допускаются сокращения, если они хорошо известны (когда вы можете легко найти расшифровку сокращения в Википедии или через поисковик).
AST, SQL.
Не так: NVDH (какие-то случайные буквы)
Неполные слова допустимы, если сокращённая форма общеупотребительна.
Вы также можете использовать сокращение, если его полное название приведено рядом с ним в комментариях.
17. Имена файлов с исходным кодом C++ должны иметь расширение .cpp. Заголовочные файлы должны иметь расширение .h.
Как писать код
1. Управление памятью.
Ручное освобождение памяти (delete) допускается только в библиотечном коде.
В библиотечном коде оператор delete можно использовать только в деструкторах.
В прикладном коде память должна освобождаться объектом, который ей владеет.
Примеры:
- Проще всего разместить объект на стеке или сделать его полем другого класса.
- Для большого числа небольших объектов используйте контейнеры.
- Для автоматического освобождения небольшого числа объектов, размещённых в куче, используйте
shared_ptr/unique_ptr.
2. Управление ресурсами.
Используйте RAII и см. выше.
3. Обработка ошибок.
Используйте исключения. В большинстве случаев достаточно только выбросить исключение, и нет необходимости его перехватывать (благодаря RAII).
В офлайновых приложениях для обработки данных часто допустимо не перехватывать исключения.
В серверах, обрабатывающих пользовательские запросы, обычно достаточно перехватывать исключения на верхнем уровне обработчика соединения.
В функциях потоков следует перехватывать и сохранять все исключения, чтобы затем повторно выбросить их в главном потоке после join.
Никогда не подавляйте исключения, не обрабатывая их. Никогда не записывайте в лог все исключения без разбора.
Если нужно игнорировать некоторые исключения, делайте это только для строго определённых, а остальные пробрасывайте дальше.
При использовании функций с кодами возврата или errno всегда проверяйте результат и выбрасывайте исключение в случае ошибки.
Вы можете использовать assert для проверки инвариантов в коде.
4. Типы исключений.
Нет необходимости использовать сложную иерархию исключений в прикладном коде. Текст исключения должен быть понятен системному администратору.
5. Выбрасывание исключений из деструкторов.
Это не рекомендуется, но допускается.
Используйте следующие подходы:
- Создайте функцию (
done()илиfinalize()), которая заранее выполнит всю работу, способную привести к возникновению исключения. Если эта функция была вызвана, в деструкторе впоследствии не должно возникать исключений. - Задачи, которые слишком сложны (например, отправка сообщений по сети), можно вынести в отдельный метод, который пользователь класса должен будет вызвать до разрушения объекта.
- Если в деструкторе возникает исключение, лучше записать его в лог, чем скрывать (если логгер доступен).
- В простых приложениях допустимо полагаться на
std::terminate(в случаяхnoexceptпо умолчанию в C++11) для завершения работы при возникновении исключений.
6. Анонимные блоки кода.
Вы можете создать отдельный блок кода внутри одной функции, чтобы сделать определённые переменные локальными, чтобы деструкторы были вызваны при выходе из блока.
7. Многопоточность.
В программах для офлайн-обработки данных:
- Старайтесь добиться максимально возможной производительности на одном ядре CPU. При необходимости затем можно распараллелить код.
В серверных приложениях:
- Используйте пул потоков для обработки запросов. На данный момент у нас не было задач, требующих переключения контекста в пространстве пользователя.
fork не используется для распараллеливания.
8. Синхронизация потоков.
Часто можно организовать работу так, чтобы разные потоки использовали разные ячейки памяти (ещё лучше — разные кэш-линии) и не требовали синхронизации (кроме joinAll).
Если синхронизация необходима, в большинстве случаев достаточно использовать мьютекс с lock_guard.
В остальных случаях используйте системные примитивы синхронизации. Не используйте активное ожидание (busy wait).
Атомарные операции следует использовать только в самых простых случаях.
Не пытайтесь реализовывать неблокирующие структуры данных, если это не ваша основная область экспертизы.
9. Указатели против ссылок.
В большинстве случаев предпочитайте ссылки.
10. const.
Используйте константные ссылки, указатели на константы, const_iterator и const-методы.
Считайте const значением по умолчанию и используйте не-const только при необходимости.
При передаче переменных по значению использование const обычно не имеет смысла.
11. unsigned.
Используйте unsigned, если это необходимо.
12. Числовые типы.
Используйте типы UInt8, UInt16, UInt32, UInt64, Int8, Int16, Int32 и Int64, а также size_t, ssize_t и ptrdiff_t.
Не используйте для чисел следующие типы: signed/unsigned long, long long, short, signed/unsigned char, char.
13. Передача аргументов.
Передавайте сложные объекты по значению, если они затем будут перемещены, и используйте std::move; передавайте по ссылке, если нужно обновлять значение в цикле.
Если функция принимает на себя владение объектом, созданным в куче, сделайте тип аргумента shared_ptr или unique_ptr.
14. Возвращаемые значения.
В большинстве случаев просто используйте return. Не пишите return std::move(res).
Если функция выделяет объект в куче и возвращает его, используйте shared_ptr или unique_ptr.
В редких случаях (обновление значения в цикле) может потребоваться вернуть значение через аргумент. В этом случае аргумент должен быть ссылкой.
15. namespace.
Нет необходимости использовать отдельный namespace для кода приложения.
Небольшим библиотекам это тоже не требуется.
Для средних и крупных библиотек поместите всё в один namespace.
В .h-файле библиотеки вы можете использовать namespace detail, чтобы скрыть детали реализации, которые не нужны коду приложения.
В .cpp-файле вы можете использовать static или анонимный namespace, чтобы скрыть символы.
Также namespace можно использовать для enum, чтобы соответствующие имена не попадали во внешний namespace (но лучше использовать enum class).
16. Отложенная инициализация.
Если для инициализации требуются аргументы, обычно не следует писать конструктор по умолчанию.
Если позже вам понадобится отложить инициализацию, вы можете добавить конструктор по умолчанию, который будет создавать невалидный объект. Или, для небольшого числа объектов, вы можете использовать shared_ptr/unique_ptr.
17. Виртуальные функции.
Если класс не предназначен для полиморфного использования, нет необходимости делать функции виртуальными. Это также относится к деструктору.
18. Кодировки.
Используйте UTF-8 везде. Используйте std::string и char *. Не используйте std::wstring и wchar_t.
19. Логирование.
Смотрите примеры в коде.
Перед коммитом удаляйте все бессмысленные и отладочные логи, а также любые другие виды отладочного вывода.
Логирования в циклах следует избегать, даже на уровне Trace.
Логи должны быть читаемыми на любом уровне логирования.
В основном используйте логирование только в прикладном коде.
Сообщения логов должны быть написаны на английском языке.
Желательно, чтобы лог был понятен системному администратору.
Не используйте ненормативную лексику в логе.
Используйте в логе кодировку UTF-8. В редких случаях можно использовать в логе не-ASCII-символы.
20. Ввод-вывод.
Не используйте iostreams во внутренних циклах, критичных для производительности приложения (и никогда не используйте stringstream).
Вместо этого используйте библиотеку DB/IO.
21. Дата и время.
См. библиотеку DateLUT.
22. include.
Всегда используйте #pragma once вместо include guards.
23. using.
using namespace не используется. Можно использовать using с чем-то конкретным. Но делайте это локально внутри класса или функции.
24. Не используйте trailing return type для функций без необходимости.
25. Объявление и инициализация переменных.
26. Для виртуальных функций пишите virtual в базовом классе, а в производных классах пишите override вместо virtual.
Неиспользуемые возможности C++
1. Виртуальное наследование не используется.
2. Конструкции, для которых в современном C++ есть удобный синтаксический сахар, например,
Платформа
1. Мы пишем код под конкретную платформу.
Но при прочих равных предпочтителен кроссплатформенный или переносимый код.
2. Язык: C++20 (см. список доступных возможностей C++20).
3. Компилятор: clang. На момент написания (март 2025 года) код компилируется с помощью clang версии не ниже 19.
Используется стандартная библиотека (libc++).
4. ОС: Ubuntu Linux, не старше Precise.
5. Код пишется для архитектуры процессора x86_64.
Набор инструкций процессора — минимальный поддерживаемый среди наших серверов. В данный момент это SSE 4.2.
6. Используйте флаги компиляции -Wall -Wextra -Werror -Weverything с некоторыми исключениями.
7. Используйте статическую компоновку со всеми библиотеками, кроме тех, которые сложно подключать статически (см. вывод команды ldd).
8. Код разрабатывается и отлаживается с релизными настройками.
Инструменты
1. KDevelop — хорошая среда разработки (IDE).
2. Для отладки используйте gdb, valgrind (memcheck), strace, -fsanitize=... или tcmalloc_minimal_debug.
3. Для профилирования используйте Linux Perf, valgrind (callgrind) или strace -cf.
4. Исходный код хранится в Git.
5. Сборка выполняется с помощью CMake.
6. Программы выпускаются в виде deb-пакетов.
7. Коммиты в master не должны ломать сборку.
Хотя рабочими считаются только отдельные ревизии.
8. Делайте коммиты как можно чаще, даже если код готов лишь частично.
Для этого используйте ветки.
Если ваш код в ветке master пока не собирается, исключите его из сборки перед push. Вам нужно будет доделать его или удалить в течение нескольких дней.
9. Для нетривиальных изменений используйте ветки и публикуйте их на сервере.
10. Неиспользуемый код удаляется из репозитория.
Библиотеки
1. Используется стандартная библиотека C++20 (допускаются экспериментальные расширения), а также фреймворки boost и Poco.
2. Не допускается использование библиотек из пакетов ОС. Также не допускается использование предустановленных в системе библиотек. Все библиотеки должны быть размещены в виде исходного кода в каталоге contrib и собираться вместе с ClickHouse. Подробности см. в разделе Рекомендации по добавлению сторонних библиотек.
3. Всегда следует отдавать предпочтение библиотекам, которые уже используются.
Общие рекомендации
1. Пишите как можно меньше кода.
2. Старайтесь выбирать самое простое решение.
3. Не пишите код, пока не будете понимать, как он будет работать и как будет устроен внутренний цикл.
4. В простейших случаях используйте using вместо классов или структур.
5. По возможности не определяйте конструкторы копирования, операторы присваивания, деструкторы (кроме виртуального, если класс содержит хотя бы одну виртуальную функцию), конструкторы перемещения или операторы присваивания перемещением. Другими словами, сгенерированные компилятором функции должны работать корректно. Можно использовать default.
6. Упрощение кода поощряется. По возможности минимизируйте объём кода.
Дополнительные рекомендации
1. Явное указание std:: для типов из stddef.h
не рекомендуется. Другими словами, мы рекомендуем писать size_t вместо std::size_t, потому что так короче.
Тем не менее, допускается добавлять std::.
2. Явное указание std:: для функций из стандартной библиотеки C
не рекомендуется. Другими словами, пишите memcpy вместо std::memcpy.
Причина в том, что есть похожие нестандартные функции, такие как memmem. Мы иногда используем эти функции. Эти функции отсутствуют в namespace std.
Если вы будете везде писать std::memcpy вместо memcpy, то memmem без std:: будет выглядеть странно.
Тем не менее, вы по-прежнему можете использовать std::, если вам так больше нравится.
3. Использование функций из C, когда те же самые доступны в стандартной библиотеке C++.
Это допустимо, если так более эффективно.
Например, используйте memcpy вместо std::copy для копирования больших блоков памяти.
4. Многострочные аргументы функций.
Допускается любой из следующих стилей переноса: