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 может осуществлять дополнительные операции.