Перейти к основному содержимому
Перейти к основному содержимому

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

Общие рекомендации

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

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

1. Большая часть форматирования выполняется автоматически с помощью clang-format.

2. Отступ — 4 пробела. Настройте свою среду разработки так, чтобы при нажатии клавиши таб добавлялось четыре пробела.

3. Открывающая и закрывающая фигурные скобки должны быть на отдельной строке.

inline void readBoolText(bool & x, ReadBuffer & buf)
{
    char tmp = '0';
    readChar(tmp, buf);
    x = tmp != '0';
}

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

inline size_t mask() const                { return buf_size() - 1; }
inline size_t place(HashValue x) const    { return x & mask(); }

5. Для функций. Не добавляйте пробелы вокруг скобок.

void reinsert(const Value & x)
memcpy(&buf[place_value], &x, sizeof(x));

6. В выражениях if, for, while и других добавляется пробел перед открывающей скобкой (в отличие от вызовов функций).

for (size_t i = 0; i < rows; i += storage.index_granularity)

7. Добавляйте пробелы вокруг бинарных операторов (+, -, *, /, %, ...) и тернарного оператора ?:.

UInt16 year = (s[0] - '0') * 1000 + (s[1] - '0') * 100 + (s[2] - '0') * 10 + (s[3] - '0');
UInt8 month = (s[5] - '0') * 10 + (s[6] - '0');
UInt8 day = (s[8] - '0') * 10 + (s[9] - '0');

8. Если происходит перенос строки, разместите оператор на новой строке и увеличьте отступ перед ним.

if (elapsed_ns)
    message << " ("
        << rows_read_on_server * 1000000000 / elapsed_ns << " rows/s., "
        << bytes_read_on_server * 1000.0 / elapsed_ns << " MB/s.) ";

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

dst.ClickLogID         = click.LogID;
dst.ClickEventID       = click.EventID;
dst.ClickGoodEvent     = click.GoodEvent;

10. Не используйте пробелы вокруг операторов ., ->.

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

11. Не используйте пробел для отделения унарных операторов (--, ++, *, &, ...) от аргумента.

12. Ставьте пробел после запятой, но не перед ней. То же правило относится к точке с запятой внутри выражения for.

13. Не используйте пробелы для отделения оператора [].

14. В выражении template <...> используйте пробел между template и <; пробелов после < или перед > быть не должно.

template <typename TKey, typename TValue>
struct AggregatedStatElement
{}

15. В классах и структурах пишите public, private и protected на одном уровне с class/struct, остальные строки кода отступайте.

template <typename T>
class MultiVersion
{
public:
    /// Version of object for usage. shared_ptr manage lifetime of version.
    using Version = std::shared_ptr<const T>;
    ...
}

16. Если одно и то же namespace используется для всего файла, и ничего другого значительного нет, отступ внутри namespace не требуется.

17. Если блок для if, for, while или другого выражения состоит из одного statement, фигурные скобки являются необязательными. Вместо этого разместите statement на отдельной строке. Это правило также применимо к вложенным if, for, while и т.д.

Но если внутренний statement содержит фигурные скобки или else, внешний блок должен быть записан в фигурных скобках.

/// Finish write.
for (auto & stream : streams)
    stream.second->finalize();

18. В конце строк не должно быть пробелов.

19. Исходные файлы кодируются в UTF-8.

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

<< ", " << (timer.elapsed() / chunks_stats.hits) << " μsec/hit.";

21. Не пишите несколько выражений в одной строке.

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

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

24. A const (относится к значению) должно быть записано перед именем типа.

//correct
const char * pos
const std::string & s
//incorrect
char const * pos

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

//correct
const char * pos
//incorrect
const char* pos
const char *pos

26. При использовании шаблонных типов используйте для их создания ключевое слово using (за исключением простейших случаев).

Другими словами, параметры шаблона указываются только в using и не повторяются в коде.

using можно объявить локально, например, внутри функции.

//correct
using FileStreams = std::map<std::string, std::shared_ptr<Stream>>;
FileStreams streams;
//incorrect
std::map<std::string, std::shared_ptr<Stream>> streams;

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

//incorrect
int x, *y;

28. Не используйте C-style приведения.

//incorrect
std::cerr << (int)c <<; std::endl;
//correct
std::cerr << static_cast<int>(c) << std::endl;

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

30. Для маленьких классов и структур нет необходимости отделять декларацию метода от реализации.

То же самое относится к маленьким методам в любых классах или структурах.

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

31. Вы можете переносить строки на 140 символов, вместо 80.

32. Всегда используйте операторы префиксного инкремента/декремента, если постфикс не требуется.

for (Names::const_iterator it = column_names.begin(); it != column_names.end(); ++it)

Комментарии

1. Обязательно добавляйте комментарии ко всем нетривиальным частям кода.

Это очень важно. Написание комментария может помочь вам понять, что код не нужен или что он реализован неправильно.

/** Part of piece of memory, that can be used.
  * For example, if internal_buffer is 1MB, and there was only 10 bytes loaded to buffer from file for reading,
  * then working_buffer will have size of only 10 bytes
  * (working_buffer.end() will point to position right after those 10 bytes available for read).
  */

2. Комментарии могут быть настолько подробными, насколько это необходимо.

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

/** Parses and executes the query.
*/
void executeQuery(
    ReadBuffer & istr, /// Where to read the query from (and data for INSERT, if applicable)
    WriteBuffer & ostr, /// Where to write the result
    Context & context, /// DB, tables, data types, engines, functions, aggregate functions...
    BlockInputStreamPtr & query_plan, /// Here could be written the description on how query was executed
    QueryProcessingStage::Enum stage = QueryProcessingStage::Complete /// Up to which stage process the SELECT query
    )

4. Комментарии должны быть написаны только на английском языке.

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

6. Не добавляйте комментарии, которые не предоставляют дополнительной информации. В частности, не оставляйте пустые комментарии, как этот:

/*
* Procedure Name:
* Original procedure name:
* Author:
* Date of creation:
* Dates of modification:
* Modification authors:
* Original file name:
* Purpose:
* Intent:
* Designation:
* Classes used:
* Constants:
* Local variables:
* Parameters:
* Date of creation:
* Purpose:
*/

Пример взят из ресурса http://home.tamk.fi/~jaalto/course/coding-style/doc/unmaintainable-code/.

7. Не пишите мусорные комментарии (автор, дата создания и т.д.) в начале каждого файла.

8. Однострочные комментарии начинаются с трех косых черт: ///, а многострочные комментарии начинаются с /**. Эти комментарии считаются "документацией".

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

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

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

11. Удалите закомментированные части кода перед коммитом.

12. Не используйте ненормативную лексику в комментариях или коде.

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

/// WHAT THE FAIL???

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

///******************************************************

15. Не начинайте обсуждения в комментариях.

/// Why did you do this stuff?

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

/// for

Имена

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

size_t max_block_size;

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

std::string getName() const override { return "Memory"; }

3. Для имен классов (структур) используйте CamelCase, начинающийся с заглавной буквы. Суффиксы, кроме I, не используются для интерфейсов.

class StorageMemory : public IStorage

4. using именуются так же, как классы.

5. Имена шаблонных аргументов типов: в простых случаях используйте T; T, U; T1, T2.

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

template <typename TKey, typename TValue>
struct AggregatedStatElement

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

template <bool without_www>
struct ExtractDomain

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

class IProcessor

8. Если вы используете переменную локально, можете использовать короткое имя.

Во всех остальных случаях используйте имя, описывающее значение.

bool info_successfully_loaded = false;

9. Имена define и глобальных констант должны использовать ALL_CAPS с подчеркиваниями.

#define MAX_SRC_TABLE_NAMES_TO_STORE 1000

10. Имена файлов должны использовать тот же стиль, что и их содержимое.

Если файл содержит единственный класс, назовите файл так же, как класс (CamelCase).

Если файл содержит единственную функцию, назовите файл так же, как функция (camelCase).

11. Если имя содержит аббревиатуру, то:

  • Для имен переменных аббревиатура должна использовать строчные буквы mysql_connection (а не mySQL_connection).
  • Для имен классов и функций следует сохранять заглавные буквы в аббревиатуре MySQLConnection (а не MySqlConnection).

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

FileQueueProcessor(
    const std::string & path_,
    const std::string & prefix_,
    std::shared_ptr<FileHandler> handler_)
    : path(path_),
    prefix(prefix_),
    handler(handler_),
    log(&Logger::get("FileQueueProcessor"))
{
}

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

13. Нет различий в названиях локальных переменных и членов класса (префиксы не требуются).

timer (not m_timer)

14. Для констант в enum используйте CamelCase с заглавной буквой. Также приемлемо использование ALL_CAPS. Если enum не локальный, используйте enum class.

enum class CompressionMethod
{
    QuickLZ = 0,
    LZ4     = 1,
};

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

not T_PAAMAYIM_NEKUDOTAYIM

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

AST, SQL.

Не NVDH (некоторые случайные буквы)

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

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

17. Имена файлов с кодом C++ должны иметь расширение .cpp. Заголовочные файлы должны иметь расширение .h.

Как писать код

1. Управление памятью.

Ручное освобождение памяти (delete) может использоваться только в библиотечном коде.

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

В коде приложения память должна освобождаться объектом, которому она принадлежит.

Примеры:

  • Самый простой способ — поместить объект в стек или сделать его членом другого класса.
  • Для большого количества маленьких объектов используйте контейнеры.
  • Для автоматического освобождения небольшого количества объектов, которые находятся в куче, используйте shared_ptr/unique_ptr.

2. Управление ресурсами.

Используйте RAII и смотрите выше.

3. Обработка ошибок.

Используйте исключения. В большинстве случаев вам просто нужно выбросить исключение и не нужно его ловить (из-за RAII).

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

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

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

/// If there weren't any calculations yet, calculate the first block synchronously
if (!started)
{
    calculate();
    started = true;
}
else /// If calculations are already in progress, wait for the result
    pool.wait();

if (exception)
    exception->rethrow();

Никогда не скрывайте исключения, не обработав их. Никогда просто не помещайте все исключения в лог.

//Not correct
catch (...) {}

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

catch (const DB::Exception & e)
{
    if (e.code() == ErrorCodes::UNKNOWN_AGGREGATE_FUNCTION)
        return nullptr;
    else
        throw;
}

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

if (0 != close(fd))
    throw ErrnoException(ErrorCodes::CANNOT_CLOSE_FILE, "Cannot close file {}", file_name);

Вы можете использовать assert для проверки инвариантов в коде.

4. Типы исключений.

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

5. Выбрасывание исключений из деструкторов.

Это не рекомендуется, но допускается.

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

  • Создайте функцию (done() или finalize()), которая выполнит всю работу заранее, что может привести к исключению. Если эта функция была вызвана, позже в деструкторе исключений быть не должно.
  • Слишком сложные задачи (например, отправка сообщений по сети) можно поместить в отдельный метод, который пользователь класса должен будет вызвать перед уничтожением.
  • Если в деструкторе есть исключение, лучше его зафиксировать, чем скрывать (если логгер доступен).
  • В простых приложениях допустимо полагаться на std::terminate (для случаев noexcept по умолчанию в C++11) для обработки исключений.

6. Анонимные кодовые блоки.

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

Block block = data.in->read();

{
    std::lock_guard<std::mutex> lock(mutex);
    data.ready = true;
    data.block = block;
}

ready_any.set();

7. Многопоточность.

В программах офлайн-обработки данных:

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

В серверных приложениях:

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

Форк не используется для параллелизации.

8. Синхронизация потоков.

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

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

В других случаях используйте системные примитивы синхронизации. Не используйтеBusy wait.

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

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

9. Указатели vs ссылки.

В большинстве случаев предпочтительнее использовать ссылки.

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.

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

using AggregateFunctionPtr = std::shared_ptr<IAggregateFunction>;

/** Allows creating an aggregate function by its name.
  */
class AggregateFunctionFactory
{
public:
    AggregateFunctionFactory();
    AggregateFunctionPtr get(const String & name, const DataTypes & argument_types) const;

15. namespace.

Нет необходимости использовать отдельный namespace для кодов приложений.

Малым библиотекам это тоже не нужно.

Для средних и крупных библиотек всё помещается в namespace.

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

В .cpp файле вы можете использовать static или анонимный namespace, чтобы скрыть символы.

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

16. Отложенная инициализация.

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

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

Loader(DB::Connection * connection_, const std::string & query, size_t max_block_size_);

/// For deferred initialization
Loader() {}

17. Виртуальные функции.

Если класс не предназначен для полиморфного использования, вам не нужно делать функции виртуальными. Это также относится к деструктору.

18. Кодировки.

Используйте UTF-8 везде. Используйте std::string и char *. Не используйте std::wstring и wchar_t.

19. Логирование.

Смотрите примеры везде в коде.

Перед коммитом удалите все бессмысленные и отладочные логи, а также любые другие виды отладочного вывода.

Логирование в циклах следует избегать, даже на уровне Trace.

Логи должны быть читаемыми на любом уровне логирования.

Логирование должно использоваться в основном в коде приложения.

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

Лог должен быть понятен системному администратору.

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

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

20. Ввод-вывод.

Не используйте iostreams во внутренних циклах, критичных для производительности приложения (и никогда не используйте stringstream).

Используйте библиотеку DB/IO вместо этого.

21. Дата и время.

Смотрите библиотеку DateLUT.

22. include.

Всегда используйте #pragma once вместо защитников включения.

23. using.

using namespace не используется. Вы можете использовать using с чем-то конкретным. Но делайте это локально внутри класса или функции.

24. Не используйте trailing return type для функций, если это не необходимо.

auto f() -> void

25. Объявление и инициализация переменных.

//right way
std::string s = "Hello";
std::string s{"Hello"};

//wrong way
auto s = std::string{"Hello"};

26. Для виртуальных функций пишите virtual в базовом классе, но пишите override, вместо virtual в производных классах.

Неиспользуемые функции C++

1. Виртуальное наследование не используется.

2. Конструкции, которые имеют удобный синтаксический сахар в современном C++, например

// Traditional way without syntactic sugar
template <typename G, typename = std::enable_if_t<std::is_same<G, F>::value, void>> // SFINAE via std::enable_if, usage of ::value
std::pair<int, int> func(const E<G> & e) // explicitly specified return type
{
    if (elements.count(e)) // .count() membership test
    {
        // ...
    }

    elements.erase(
        std::remove_if(
            elements.begin(), elements.end(),
            [&](const auto x){
                return x == 1;
            }),
        elements.end()); // remove-erase idiom

    return std::make_pair(1, 2); // create pair via make_pair()
}

// With syntactic sugar (C++14/17/20)
template <typename G>
requires std::same_v<G, F> // SFINAE via C++20 concept, usage of C++14 template alias
auto func(const E<G> & e) // auto return type (C++14)
{
    if (elements.contains(e)) // C++20 .contains membership test
    {
        // ...
    }

    elements.erase_if(
        elements,
        [&](const auto x){
            return x == 1;
        }); // C++20 std::erase_if

    return {1, 2}; // or: return std::pair(1, 2); // create pair via initialization list or value initialization (C++17)
}

Платформа

1. Мы пишем код для конкретной платформы.

Но при равных других условиях предпочтителен кросс-платформенный или переносимый код.

2. Язык: C++20 (см. список доступных функций C++20).

3. Компилятор: clang. На момент написания (март 2025) код компилируется с использованием версии clang >= 19.

Используется стандартная библиотека (libc++).

4. ОС: Linux Ubuntu, не старше Precise.

5. Код написан для архитектуры процессора x86_64.

Набор инструкций CPU является минимально поддерживаемым набором среди наших серверов. В данный момент это 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. Упрощение кода рекомендуется. Уменьшайте размер вашего кода, где это возможно.