BMSTU CS Style Guide

Стайл гайд и общий список советов при написании кода на C++ для студентов курса “Основы Программирования” кафедры ИУ5 МГТУ им. Н.Э. Баумана.

Содержание

  1. Общие правила
  2. Переменные и константы
  3. Функции
  4. Типы
  5. Операторы
  6. Условия
  7. Циклы
  8. Switch-case
  9. Namespaces
  10. Комментарии
  11. Заголовочные файлы
  12. DRY
  13. KISS

Общие правила

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

Для удобства разработки, при работе над проектом, в котором участвуют много (больше одного) разработчиков, следование стайл гайду является обязательным. Многие отдельные команды и компании имеют свои стайл гайды, например, Google C++ Style Guide.

Одной из целей курса является научить вас писать “красивый” и читаемый код. Часть описанных здесь правил являются объективными представлениями о современном написании кода (часто безотносительно языка программирования), часть субъективными.

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

Автоматизация, clang-format и clang-tidy

Многие правила, описанные в данном документе, форсируются автоматически с применением clang-format и clang-tidy.

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

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

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

Пробелы/табы

Не используйте табы для создания отступов в коде. Вместо табов для отступов используется 4 пробела. Все современные IDE и редакторы кода позволяют настроить клавишу Tab на вставку нужного количества пробелов.

Фигурные скобки и отступы

Для вставки отступов и обрамления фигурными скобками используется так называемый One True Brace style. Открывающая фигурная скобка ставится на той же строке, что и название функции/условие/цикл/структура/класс/…, а закрывающая на отдельной строке.

void func()
{
    ...
} // bad

void func() {
    ...
} // OK

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

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

Правила переноса установлены в конфиге для clang-format.

Общие правила именования

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

Не следует применять необоснованные сокращения и ничего не значащие символы (за исключением распротраненных обозначений, например, i, j, k для итерации внутри цикла, T для обозначения шаблонного типа, lhs и rhs для левого и правого аргумента в подходящих контекстах и т.п.).

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

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

struct C {
    int x;
    int y;
    int z;
}; // bad

struct Coordinates {
    int x;
    int y;
    int z;
}; // OK
void InitMA(int n); // bad

void InitMemoryAllocator(int id); // OK

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

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

bool IsPrime(int number) {
    if (number <= 1) {
        return false;
    }
    for (int i = 0; i * i < number; ++i) {
        if (number % i == 0) {
            return false;
        }
    }
    return true;
} // bad

bool IsPrime(int number) {
    if (number <= 1) {
        return false;
    }

    for (int i = 0; i * i < number; ++i) {
        if (number % i == 0) {
            return false;
        }
    }

    return true;
} // OK

bool IsPrime(int number) {
    if (number <= 1) {

        return false;

    }

    for (int i = 0; i * i < number; ++i) {

        if (number % i == 0) {

            return false;

        }

    }

    return true;
} // also bad

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

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

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

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

int a; // bad

namespace {
    const float pi = 3.14159265359;
} // OK

Goto statements

goto - оператор безусловного перехода к определенной точке программы, обозначенной именованной меткой.

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

using namespace std

Использование директивы using namespace std полностью запрещено. Как в заголовочных, так и в .cpp файлах.

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

Более подробно будет рассказано в лекциях.

STL и ООП

Использование элементов ООП запрещено. Таким образом запрещено использование классов, методов, разных конструкторов, перегрузок операторов, модификаторов доступа и т.п.

Помимо этого запрещено использование STL: std::string, любые контейнеры (std::vector, std::map, std::set, std::unordered_map и т.д.), адаптеры над контейнерами (std::queue, std::stack) и большинство других классов стандартных библиотек, за некоторыми исключениями (например, классы, связанные с потоковым вводом и выводом и манипуляцией ввода/вывода, такие как std::istream, std::ostream, std::fstream и т.п.).

Препроцессор и макросы

В общем случае использование макросов и макроконстант запрещено. Вместо этого следует использовать функции и обычные константы.

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

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

#define ARRAY_SIZE 500
  // bad

#define SUM(LHS, RHS) ((X) + (Y))
  // bad

Функция main

Функция main должна обязательно возвращать int, а также принимать аргументы командной строки (int argc, char* argv*). Если аргументы командной строки не используются, то следует просто оставить их тип или пометить атрибутом [[maybe_unused]].

void main() {
    ...
} // bad

int main(int, char**) {
    ...
    return 0;
} // OK

int main([[maybe_unused]] int argc, [[maybe_unused]] char** argv) {
    ...
    return 0;
} // OK

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

Проверка ошибок

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

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

Проверка входных данных

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

double Divide(int lhs, int rhs) {
    if (rhs == 0) {
        ... // err, OK, can't divide by zero
    }
    return lhs / rhs;
}
void PrintArg(int* arg) {
    std::cout << *arg; // bad, arg might be nullptr
}

void PrintArg(int* arg) {
    if (!arg) {
        return;
    }
    std::cout << *arg; // OK
}

Компиляция

Любая компиляция исходного кода должна сопровождаться флагами -Wall, -Wextra, -Werror и -Wpedantic.

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

Неиспользуемый код

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

Пустая строка в конце файла

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

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

Правила именования

Обычные переменные и константы внутри функций именуются через camelCase.

int variableWithMultipleWords;

Глобальные константы также именуются через camelCase, но с префиксом k в начале.

const int kGlobalConstant;
constexpr int kAnotherGlobalConstant;

Магические константы

Магическая константа - это некий литерал, семантическое (смысловое) значение которого нельзя определить без контекста.

int main(int, char**) {
    int number = 0;
    std::cin >> number;

    if (number >= 100) { // 100 - магическая константа
        return -1;
    }

    return 0;
}

Значения магических констант должны быть сложены в именованные константы. При этом допускается прямое использование некоторых литералов, таких как 0, true, false, nullptr, “” и др.

namespace {
    const int kMaxAvailableNumber = 100;
}

int main(int, char**) {
    int number = 0;
    std::cin >> number;

    if (number >= kMaxAvailableNumber) {
        return -1;
    }

    return 0;
}

Одна строка - одна переменная

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

int a = 0, b = 0, c = 0; // bad

int a = 0; // OK
int b = 0; // OK
int c = 0; // OK

Объявление и инициализация

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

int a; // bad, indeterminate value
int a = 0; // OK
int a{}; // OK, default value for int is 0
int a = 0;
int a = foo(); // bad
int b = foo(); // OK

Указатели и ссылки

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

int a;
int &r = a; // bad
int& r = a; // OK
int *p = nullptr; // bad
int* p = nullptr; // OK

Использование констант

Если переменная не предполагает дальнейшее изменение значения, то следует помечать ее как константу через const.

Использование атрибутов

Если переменная необходима, но не используется или может не использоваться в некоторых случаях, ее следует пометить атрибутом [[maybe_unused]].

[[maybe_unused]] int a = 0;

Использование auto

Использование auto в общем случае запрещено.

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

auto a = 5; // bad
auto b = foo(); // bad
  1. Использование structured binding для присвоения возвращаемого значения-структуры из функции.
struct S {
    int a;
    int b;
}

S foo();

auto [first, second] = foo(); // OK
  1. Присваивание лямбда функции переменной.
auto l = [] { return 0; }; // OK
  1. Некоторые специфические случаи при работе с лямбдами, метапрограммированием и др.

Функции

Правила именования

Функции именуются в PascalCase.

Функции, как правило, должны семантически отражать какое-то действие (то есть содержать глагол) или отвечать на некоторый вопрос.

void Directory(const char* name); // bad
void CreateDirectory(const char* name); // OK

bool IsEven(int number); // OK

Размеры

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

Божественные функции

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

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

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

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

void CreateOrRemoveFile(const char* fileName, bool createOrRemove); // bad

void CreateFile(const char* fileName); // OK
void RemoveFile(const char* fileName); // OK

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

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

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

void CreateFile(const char* fileName, const char* directory = "");
  // OK, "" is current working directory
double Pow(double number, double power = 0); // bad, unintuitive

Использование атрибутов

Если функция возвращает значение, которое нельзя игнорировать и необходимо использовать после вызова функции, то такую функцию следует помечать атрибутом [[nodiscard]] рядом с объявлением функции.

В следующем примере ожидается, что результат выполнения функции Sum должен быть каким-либо образом использован (иначе вызов функции не имеет смысла). Неиспользование возвращаемого результата функции с использованием атрибута [[nodiscard]] приведет к ошибке компиляции.

int Sum(int lhs, int rhs); // bad, the return value must be used
[[nodiscard]] Sum(int lhs, int rhs); // OK

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

int CreateUser(const char* name); // OK

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

Если функция не используется, но ее удаление нежелательно, то она должна быть помечена атрибутом [[maybe_unused]].

Использование auto

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

auto Sum(int lhs, int rhs); // bad

template<typename T, typename U>
auto Sum(T lhs, U rhs); // OK

Использование trailing return type

Trailing return type - особый вид нотации для отображения типа возвращаемого значения. Его использование не регламентировано и остается на усмотрение студента.

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

int Sum(int lhs, int rhs); // OK
auto Sum(int lhs, int rhs) -> int; // OK
int Sum(int lhs, int rhs);
auto Multiply(int lhs, int rhs) -> int; // bad, don't mix styles

Типы

Структуры

Правила именования

Структуры именуются в PascalCase.

struct S;

Поля структуры именуются в camelCase.

struct S {
    int firstField;
    int secondField;
};

Следует избегать названий вида My[Something] или Custom[Something].

Инициализация

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

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

struct S {
    int a;
    char b;
    float c;
    double d;
};

S s = {}; // a == 0, b == 0, c == 0.f, d == 0.

В общем случае следует предпочитать использование designated initializers:

S s = {
    .a = 1,
    .b = 2,
    .c = 3,
    .d = 4, // !!! comma is necessary if you to keep this formatting
};

Или:

S s = {.a = 1, .b = 2, .c = 3, .d = 4};

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

struct Coordinates {
    int x;
    int y;
    int z;
};

Coordinates coords = {1, 2, 3}; // OK

Перечисления

Правила именования

Перечисления именуются в PascalCase. Возможные значения перечисления должны также именоваться в PascalCase.

enum class TrafficLight {
    Red,
    Yellow,
    Green,
};

Использование

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

Например, идиоматически некорректно использовать bool для обозначения пола. Вместо этого следует использовать enum.

bool gender = true; // male, bad
bool gender = false; // female, bad

enum class Gender {
    Male,
    Female,
};

Gender gender = Gender::Female; // OK

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

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

enum class E {
    A = 0,
    B = 1,
}; // bad

enum class E : int {
    A = 0,
    B = 1,
}; // OK

Операторы

Унарные операторы

Между оператором и операндом не должно быть пробела.

int a = 0;
++a; // OK
++ a; // bad
int a = 0;
int b = &a; // OK
int b = & a; // bad

Бинарные операторы

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

5+5; // bad
5 + 5; // OK

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

int a = 0;
int b = a++ + ++a * a % 10 / 20; // really bad
int c = 5 + 5 / 2; // OK
int c = 5 % 5 / 2; // bad
int c = (5 % 5) / 2; // OK

Тернарные операторы

Единственный тернарный оператор в C++ - оператор ветвления ?:. Его можно использовать в тех случаях, когда нужно определить исполняемое действие на основе небольшого логического выражения. Если логика достаточно сложная и чтение такой конструкции оказывается затруднено, то следует использовать обычные условные конструкции (if/else).

int a = 10 > 5 ? 0 : 1; // OK
int a = 10 > 5 ? 0 : 7 < 3 ? 2 : 1; // bad, hard to read

Условия

Сравнения

Так как каждый тривиальный тип можно неявно привести к bool, где 0 - это false, а любое другое значение - это true, то ко всем таким переменным можно применить оператор логического отрицания (!). Это приводит к возможности использования некоторых синтаксических конструкций, которые будут разобраны ниже.

С числовыми типами

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

int a = 0;
if (a == 5) {
    ...
} // OK

if (a == 0) {
    ...
} // OK

if (!a) {
    ...
} // bad

С bool

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

bool b = true;

if (b == true) {
    ...
} // bad

if (b) { // if b is true
    ...
} // OK

if (!b) { // if b is false
    ...
} // OK

С указателями

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

char* p = nullptr;

if (p == nullptr) {
    ...
} // bad

if (!p) {
    ...
} // OK

Однострочные условия

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

if (true)
    std::cout << "true" << std::endl // bad

if (true) {
    std::cout << "true" << std::endl
} // OK

if/else

Следует избегать использование else там, где это возможно.

void foo(int a) {
    if (a == 5) {
        std::cout << "a == 5" << std::endl;
    } else {
        std::cout << "a != 5" << std::endl;
    }
} // bad

void foo(int a) {
    if (a == 5) {
        std::cout << "a == 5" << std::endl;
        return;
    }

    std::cout << "a != 5" << std::endl;
} // OK

Инверсия условий (return early)

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

void foo(int* a, int* b, int* c) {
    if (a) {
        if (b) {
            if (c) {
                ...
            }
        }
    }
} // OK

void foo(int* a, int* b, int* c) {
    if (!a || !b || !c) {
        return;
    }

    ...
} // OK

Нотация Йоды

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

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

Использование атрибутов

Использования атрибутов указания компилятору вероятности захода в тело условия [[likely]] и [[unlikely]] не запрещено напрямую, однако, не рекомендуется к использованию за отсутствием необходимости в рамках выполнения заданий на курсе.

Циклы

Однострочные циклы

Однострочные циклы должны быть обернуты в фигурные скобки.

while (true)
    ... // bad

while (true) {
    ...
} // OK

Использование разных видов циклов

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

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

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

Если нужно обеспечить как минимум одну итерацию цикла, то можно обратиться к конструкции do/while.

Если нужно проитерироваться по значениям массива, размер которого известен на стадии компиляции, то стоит использовать range-based for loop.

Switch-case

Правила использования

Switch-case конструкцию следует использовать в тех случаях, когда нужно проверить одну и ту же целочисленную переменную с разными значениями. Тогда множество условных конструкций if/else заменяется на switch-case.

int a = 0;

if (a == 0) {
    ...
} else if (a == 1) {
    ...
} else if (a == 2) {
    ...
} // bad

switch (a) {
    case 1:
        ...
        break;
    case 2:
        ...
        break;
    case 3:
        ...
        break;
    default:
        ...
        break;
} // OK

Switch-case предпочитаемый способ работы с переменными с типом enum.

Почти всегда следует предусматривать поведение программы для тех случаев, когда switch-case не покрывает все возможные значения. Для этого следует использовать метку default.

break & fallthrough

В конце каждого case по умолчанию должен стоять break. Его отсутствие приведет к ошибке компиляции.

Если “падение” в следующий case является желаемым поведением, то следует использовать атрибут [[fallthrough]].

int a = 0;

switch (a) {
    case 1:
        ...
    case 2:
        ...
        break;
} // bad, compilation error

switch (a) {
    case 1:
        ...
        [[fallthrough]];
    case 2:
        ...
        break;
} // OK

Namespaces

Правила именования

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

Как в случае и с типами, следует избегать названий начинающихся с My, Custom и т.п.

Анонимные неймспейсы

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

Псевдонимы

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

// file main.cpp
namespace fs = std::filesystem; // OK
namespace a = std::filesystem; // bad
// file SomeHeader.h
namespace fs = std::filesystem; // bad

using ::

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

// file main.cpp
namespace Vec {
    struct Vector {
        ...
    };
}

using Vec::Vector; // OK

Комментарии

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

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

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

Правила именования

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

Название файла должно быть в PascalCase и отображать семантически то, что описано в данном файле.

Vector.h - описан тип вектора и функции над работой с ним
Vector.cpp - реализация

Vector.h - только заголовочный файл, в случае если реализуемый вектор - шаблонный тип

Расширения файлов

Для заголовочных файлов используется либо расширение .h, либо .hpp (в зависимости от предпочтений). Смешивать вместе разные расширения запрещено.

Файлы с исходным кодом должны иметь расширение .cpp.

Порядок подключения

Заголовочные файлы делятся на 4 типа:

  1. Парный заголовочный файл, название которого соответствует .cpp файлу, в котором подключается этот файл;
  2. Заголовочные файлы проекта - все остальные заголовочные файлы, созданные в проекте;
  3. Файлы сторонних библиотек - все заголовочные файлы, которые относятся к нестандартной библиотеке;
  4. Файлы стандартной библиотеки;

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

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

// file Vector.cpp
#include "Vector.h" // парный заголовочный файл

#include "HeaderOne.h"
#include "MyHeader.h" // заголовочные файлы проекта

#include <QObject>
#include <QString> // заголовочные файлы сторонних библиотек

#include <cmath>
#include <iomanip>
#include <iostream> // заголовочные файлы стандартной библиотеки

Header guards & #pragma once

Заголовочные файлы обязательно должны содержать либо header guards, либо #pragma once для предотвращения повторного включения заголовочных файлов.

Макроконстанта для header guards должна быть полностю уникальной для каждого файла. Например, можно использовать относительный путь из корня репозитория.

// file include/foo/bar.hpp

#ifndef INCLUDE_FOO_BAR_HPP
#define INCLUDE_FOO_BAR_HPP

// file content

#endif  // INCLUDE_FOO_BAR_HPP

Подключение заголовочных файлов из C

Заголовочные файлы из библиотеки для языка C подключаются как <c’name’>, а не <name.h>.

#include <math.h> // bad
#include <cmath> // OK

DRY

DRY - Don’t Repeat Yourself. Принцип разработки, согласно которому следует избегать дублирование кода в приложении.

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

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

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

KISS

KISS - Keep It Simple, Stupid. Принцип разработки, согласно которому следует предпочитать простые конструкции сложным.

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

Метапрограммирование и вычисления времени компиляции (TBD)

Общие правила

Метапрограммирование на шаблонах

SFINAE

type_traits

constexpr

constexpr константы

constexpr функции

constexpr условия

consteval

constinit

Функциональное программирование (TBD)

Лямбда функции