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

Coding Style

Правила оформления кода вводятся не для удобства написания, а для удобства чтения. Часть правил взята из руководства Google по оформлению кода на С++. Часть правил добавлена от себя, они не имеют обоснования, просто я тут царь и бог, и мне так нравится.

Основное правило

Ориентируйся на существующий код

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

Рекомендуемая длина строк – менее 80 символов, в том числе для комментариев.

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

Для отступов используются пробелы. Стандартный отступ – четыре пробела.

Директивы препроцессора #define, #if, #ifdef и другие пишутся с начала строки.

Содержимое пространства имен пишется без отступов.

Ключевые слова public, protected, private внутри классов пишутся без отступов.

Числа с плавающей запятой пишутся с десятичной точкой и числами по обе стороны от неё.

Некрасиво:

double a = 1.;
double b = -.5;
double c = 123e4;

Хорошо:

double a = 1.0;
double b = -0.5;
double c = 123.0e4;

Рекомендуемое форматирование условий:

if (condition1) {
// ...
}
else if (condition2) {
// ...
}
else {
// ...
}

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

if (condition) break;

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

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

По сути

Настройте автоформатирование в вашей IDE. Отступы – пробелами, namespace – без отступов.

Именование

Имена типов начинаются с прописной буквы, каждое новое слово также начинается с прописной буквы, подчеркивания не используются (CamelCase). Аббревиатуры (кроме очень коротких) пишутся строчными буквами. Более 4 заглавных букв подряд – некрасиво. К примеру, SomeDB (некоторая база данных) – нормально, но CsvFile лучше CSVFile. Имена всех типов – классов, структур, псевдонимов, перечислений, параметров шаблонов – именуются в таком же стиле.

Имена файлов строчными буквами через нижнее подчеркивание: some_db.h, some_db.cpp.

Имена переменных, включая параметры функций и члены классов, пишутся строчными буквами с подчеркиваниями между словами (snake_case). Члены данных классов (не структур) дополняются префиксом m_.

class Foo {
public:
int size() { return m_size; }
double value() { return m_value; }

protected:
int m_size;
double m_value;
}

Такое правило позволяет легко опознавать члены класса внутри функций класса. Поля класса обычно являются закрытыми и имеют функции доступа get/set, get функции именуются аналогично члену класса, к которому осуществляется доступ, но без приставки m_. Венгерская нотация с добавлением типов к переменным не приветствуется (алло, у нас статическая типизация). Однако, часто используется префикс n_, который обозначает количество: n_nodes, n_faces, и так далее.

Названия функций также пишутся в snake_case.

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

Если коротко

Имена типов – CamelCase, всё остальное – snake_case.

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

Для заголовочных файлов используется расширение .h. Обычно одному заголовочному файлу соответствует единственный cpp-файл с исходным кодом и таким же именем. Иногда могут добавляться дополнительные cpp-файлы, которые соответствуют разной функциональности для большого класса. К примеру, файлу mesh.h с объявлением класса Mesh может соответствовать набор cpp-файлов с именами mesh.cpp, mesh_io.cpp, mesh_build.cpp, mesh_refine.cpp и так далее.

Порядок объявления заголовочных файлов:

  • Системные заголовочные;

  • Заголовочные файлы проекта в алфавитном порядке;

  • Соответствующий .h файл.

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

Пространства имен

Пространства имен имеют короткие простые имена. В проекте соответстувуют ключевым папкам внутр zephyr.

Вложенные пространства имен объединяются:

namespace foo::bar { }

Не используйте using-директиву в заголовочных файлах. Директиву using namespace std вообще нигде не используйте.

Не стоит использовать псевдонимы пространств имён в заголовочных файлах.

Когда определения внутри cpp-файла не используются в других исходных файлах, размещайте такие определения в безымянном пространстве имён

Классы и структуры

Секции внутри классов размещаются в следующем порядке: public, protected, private. Перед этими ключевыми словами отступ не ставится. Поля класса в идеале должны быть приватными (инкапсуляция, все дела). Поля класса именуются с префикса _m. Классы нужны чтобы хранить инварианты. Поэтому у них, в отличие от структур, обычно есть private поля, get/set функции, конструкторы со сложной инициализацией.

Структуры используются для пассивных объектов, которые просто хранят данные. Обычно структуры используются, чтобы передать набор аргументов в функцию или вернуть набор значений из функции. Члены данных структур обычно являются открытыми, для них не пишутся get/set функции, а члены данных структур именуются без приставки m_.

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

struct line_options {
std::optional<std::string> linestyle = std::nullopt;
std::optional<double> linewidth = std::nullopt;
std::optional<std::string> color = std::nullopt;
// ...
};


plt.plot(x, y, {.linestyle="dashed", .color="black"});

Функции

Результат выполнения C++ функции можно получить через возвращаемое значение и как выходной параметр. Почти всегда рекомендуется возвращать значения. В современном С++ несколько возвращаемых значений можно объединять в кортежи и использовать структурное связывание (structured binding):

std::tuple<double, double> foo(double x) {
return { std::sin(x), std::cos(x) };
}

auto[a, b] = foo(1.0);

double c, d;
std::tie(c, d) = foo(1.0);

Ещё лучше вместо кортежей использовать небольшие структуры:

struct deriv_t {
double val, d_dx;
};

deriv_t foo(double x) {
return {std::sin(x), std::cos(x)};
}

auto[a, b] = foo(1.0);

double d_dx = foo(1.0).d_dx;

Большие объекты следует передавать в функции по константной ссылке:

void foo(const std::vector<double>& x)

Паттерны проектирования

Статический фабричный метод

Идиома, которая часто встречается в проекте:

class Foo {
static Foo Empty();
static Foo Complex();
static Foo Random();
}

Foo foo = Foo::Random();

Подходит для создания экземляров небольших классов. Используется в том числе в библиотеке Eigen. В проете неоднократно встречаются вызовы:

Vector3d a = Vector3d::Zero();
Vector3d b = Vector3d::UnitX();

Расширением является статичный фабричный метод для создания умных указателей:

class Foo {
public:
using Ptr = std::shared_ptr<Foo>;

Foo(args1);
Foo(args2);

template <class... Args>
static Foo::Ptr create(Args&&... args){
return std::make_shared<Cuboid>(std::forward<Args>(args)...);
}
}

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

Foo::Ptr a = Foo::create(args);

Фабричный метод create помимо банального вызова make_shared может осуществлять дополнительные операции.