Skip to content

Yet another С++ coding guide, but, IMHO, it has good enough explanations.

Notifications You must be signed in to change notification settings

yurablok/cpp-coding-guide

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 

Repository files navigation

cpp-coding-guide

Yet another С++ coding guide, but, IMHO, having good enough explanations.
Revision 1. 2023-04-25

ENG RUS

1. Main goals

TODO: English translation.

1. Основные цели

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

2. Именование

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

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

Таким образом, название состоит из 3-х частей, разделённых символом "_". Левая и правая часть могут отсутствовать в зависимости от контекста.

Префикс указывает на вариант хранения, в порядке уменьшения приоритета:

  1. Для глобальных: g_
  2. Для thread-local: t_
  3. Для статических: s_
  4. Для приватных и защищённых полей структур и классов: m_
  5. Для публичных полей структур и классов: не используется.
  6. Для локальных: не используется.

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

  1. Type Driven Development - целая методология.
  2. time_ms duration; - отдельный тип данных, проверки на этапе компиляции.
  3. int64_t duration_ms; - суффикс, видно в любом месте работы с переменной.
  4. int64_t duration; // ms - комментарий, видно лишь в месте декларации, или если IDE умеет выводить подсказки.
  5. int64_t duration; - метаинформация о единицах измерения отсутствует.

Следовательно:

  • 1 и 2 - использовать по ситуации.
  • 3 - рекомендуется, так как это оптимальный вариант между затратами времени и количеством полезной информации.
  • 4 и 5 - не использовать.

Следовательно, суффикс указывает на единицы измерения если они применимы. Например: _ms.

Некоторые нюансы по единицам измерения:

  • Проценты % можно сократить как _prc.
  • something PER something -> something/something -> something IN something
    m/s -> _mIs
    Возможные примеры исключений: _fps, _mph, так как они привычны.

Некоторые нюансы по названиям переменных:

  • idx vs it. Для переменных-итераторов полезно понимать вариант доступа:
    itemIt (iterator), последовательный доступ.
    itemIdx (index), смещение от 0, случайный доступ.
    Возможный пример исключения: for (... y ...) { for (... x ...).

2.2. Типы данных

Рекомендуется: PascalCase.

2.3. Функции и методы

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

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

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

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

Рекомендуется: snake_case.

2.5. Макросы

Рекомендуется: UPPER_CASE_suffix.

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

UPPER_CASE используется только для макросов.

3. Типы данных

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

Использовать только типы с фиксированным размером (<cstdint>, int8_t..int64_t, uint8_t..uint64_t), так как это минимизирует количество проблем. При этом, std:: писать не следует, так как эти типы находятся также и в глобальном пространстве имён, и указание пространства std:: функционально ничего не меняет, а лишь добавляет символы в коде.

При желании, можно добавить, например, int8v2_t, uint32v4_t для определения векторных типов на 2 и 4 составляющие соответственно.

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

Для написания типизированных констант, рекомендуется использовать макросы (U)INT8_C..(U)INT64_C, (U)INT8_MIN..(U)INT64_MIN, (U)INT8_MAX..(U)INT64_MAX, так как это наиболее кроссплатформенные варианты.

Если код шаблонный, тогда конечно используется std::numeric_limits.

4. Константы

Основная логика метаинформации здесь - нижний регистр для специальных разделителей (0x0 0b0 0.0f 0.0e+0), верхний регистр для цифр (0xABCD). Перфекционизм.

Для плавающей точки, всегда пишется целая и дробная часть, например: 0.0f.

5. Структура кода

5.1. Иммутабельность

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

5.2. Ширина строк

Нормальная: до 80 символов. Рекомендованная: до 100 символов.

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

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

5.3. Комментарии

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

5.4. Отступы и скобки

Для отступов использовать 4 пробела, так как 2 слишком мало для императивного кода, а 8 слишком много. Табуляция создаёт известные проблемы.

Открывающие скобки ({, <, (, [) НЕ располагать на отдельной строке для всех языковых конструкций. Далее будут приведены разные примеры.

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

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

int32_t foobar(int32_t aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
        int32_t bbbbbbbbbbbbbbbb, int32_t cccccccccccccccccccccccccc,
        int32_t dddddddddddddddddddddddddddddddddddddddddd) {
    body();
}

Подвыражения нужно явно выделять строками и отступами:

if ((conditionA & conditionB)
        | (conditionC & conditionD)
        | (conditionE & conditionF)) {
    ...        
}

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

action(std::make_unique<Class>(  // +2
    argumentA, argumentB, argumentC,
    calculate(  // +1
        argumentD, argumentE
    )  // -1
));  // -2

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

Комплексный пример:

namespace some {

// class Foo
//     : public Bar, public Baz {

class Foo : public Bar, public Baz {
public:
    Foo(const int32_t a, const int32_t b, const int32_t c, const int32_t d)
            : m_a(a), m_b(b)
            , m_c(c), m_d(d) {
        ...
    }

    const Object* getObject() const {
        return condition
            ? &s_someDefault
            : condition
                ? m_someExist.get()
                : nullptr;
    }
    void setObject(const Object* object);

signals:
    // Qt-variant
    void sigSome(); // Название функций-сигналов должно начинаться на "sig"
    // Another variant
    utils::Signal sigSome;

public slots:
    void onSome() { // Название функций-слотов должно начинаться на "on"
        if (condition) {
            ...
        }
        else {
            ...
        }

        switch (variable) {
        case variantA:
            if (condition) {
                break;
            }
            break;
        case variantB: {
            break;
        }
        default:
            break;
        }
    }

    void onProcess10Hz(); // Если период вызова фиксированный, пишите его в названии
}; // class Foo

} // namespace some

5.5. Пустые разделяющие строки

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

5.6. Логические проверки

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

if (boolean)                if (!boolean)
if (number != 0)            if (number == 0)
if (pointer != nullptr)     if (pointer == nullptr)

5.7. Порядок включения файлов

#include "implementation.h" // The header for this implementation.cpp is always topmost

#include <iostream> // std libs are first
#include <vector>
#include <random>

#ifdef _WIN32
#   define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
#   define NOMINMAX // Fixes the conflicts with STL
#   include <Windows.h>
#else // deps are in the middle
#   define _LARGEFILE_SOURCE
#   include <posix.h>
#endif

#include "deps/utils.h" // locals are last
#include "logic.h"

...
...

5.8. Передача больших нетривиально-копируемых объектов

Это объекты больше чем 2 * sizeof(void*), или объекты со сложной логикой копирования.

Если только перемещение:
    function(type&& value)
Если только копирование:
    function(type value)
Если только чтение:
    function(const type& value)
Если универсальная передача:
    Если публичный интерфейс с одним параметром:
        function(type&& value)
        function(const type& value)
    Иначе, для упрощения, можно:
        function(type value)

Вариант исключения в соответствии с критериями выше:
std::string_view лучше передавать по значению, так как передача таких объектов оптимизируется через два регистра.

5.9. Передача указателей

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

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

  • Когда функция в базовом классе - virtual.
  • Когда функция в производном классе, но дальнейшее наследование не запрещается - override.
  • Когда функция в производном классе, и дальнейшее наследование запрещается - final.

5.11. Макросы

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

Дополнительный отступ между символом # и ключевым словом макроса, позволяет дополнительно показать контекстную связь между этим макросом и участком кода, в котором он расположен. Ещё одним преимуществом такого отступа является то, что участок кода визуально не разбивается текстом, который начинается с начала строки.
Следует выравнивать ключевое слово макроса посередине между линией отступа тела кода в котором он располагается, и этой линией-минус-1-отступ. То есть, получается минус 2 пробела от отступа тела кода.

void function() {
    while (condition) {
        body
#     ifdef A
        ...
#     endif
        ...
    }
}

В случае, когда места мало, а вложенности много, можно шагать по одному пробелу:

class Class() {
public:
    ...
private:
# if sizeof(void*) == sizeof(uint64_t)
#  ifdef NDEBUG
    utils::FastPimpl<128> m_impl;
#  else
    utils::FastPimpl<160> m_impl;
#  endif // NDEBUG
# else
#  ifdef NDEBUG
    utils::FastPimpl<96> m_impl;
#  else
    utils::FastPimpl<112> m_impl;
#  endif // NDEBUG
# endif
};

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

6.1. DRY - Don't Repeat Yourself

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

6.2. Вложенность

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

bool updateState(const State newState) {
    if (!device.isReady()) {
        return false;
    }
    if (m_prevState == newState) {
        return false;
    }
    switch (newState) {
    case State::Undefined:
    default:
        return false;
    case ...:
        ...
    }
    std::cout << "state changed: " << str(m_prevState) << " -> " << str(newState) << "\n";
    m_prevState = newState;
    return true;
}

6.3. Комбинаторика

Одно из многих правил, которое следует из правила DRY:
При написании кода содержащего много вариантов, предпочитайте O(m + n) вместо O(m * n):

// O(m + n)
switch (message_part0) {
case aa: message += "aa"; break;
case bb: message += "bb"; break;
}
switch (message_part1) {
case cc: message += "cc"; break;
case dd: message += "dd"; break;
}
switch (message_part2) {
case ee: message += "ee"; break;
case ff: message += "ff"; break;
case gg: message += "gg"; break;
}
// O(m * n)
switch (message_code) {
case aa_cc_ee: message = "aa_cc_ee"; break;
case aa_cc_ff: message = "aa_cc_ff"; break;
case aa_cc_gg: message = "aa_cc_gg"; break;
case aa_dd_ee: message = "aa_dd_ee"; break;
case aa_dd_ff: message = "aa_dd_ff"; break;
case aa_dd_gg: message = "aa_dd_gg"; break;
case bb_cc_ee: message = "bb_cc_ee"; break;
case bb_cc_ff: message = "bb_cc_ff"; break;
case bb_cc_gg: message = "bb_cc_gg"; break;
case bb_dd_ee: message = "bb_dd_ee"; break;
case bb_dd_ff: message = "bb_dd_ff"; break;
case bb_dd_gg: message = "bb_dd_gg"; break;
}

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

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

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

6.5. Ускорение компиляции

Старайтесь использовать техники ускорения компиляции, в порядке уменьшения приоритета:

  1. forward declaration
  2. precompiled header
  3. fast pimpl
  4. pimpl

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

TODO: C++20 modules

About

Yet another С++ coding guide, but, IMHO, it has good enough explanations.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published