#.Но если всё тело функции достаточно короткое (один statement) - при желании, его можно целиком разместить на одной строке. При этом, вокруг фигурных скобок ставятся пробелы (кроме пробела на конце строки).
/// Конкретная версия объекта для использования. shared_ptr определяет время жизни версии.
using Version = Ptr;
#. Если на весь файл один namespace и кроме него ничего существенного нет - то отступ внутри namespace не нужен.
#. Если блок для выражения if, for, while... состоит из одного statement-а, то фигурные скобки писать не обязательно. Вместо этого поместите statement на отдельную строку. Этим statement-ом также может быть вложенный if, for, while... Но если внутренний statement содержит фигурные скобки или else, то у внешнего блок следует писать в фигурных скобках.
..code-block:: cpp
/// Если файлы не открыты, то открываем их.
if (streams.empty())
for (const auto & name : column_names)
streams.emplace(name, std::make_unique<Stream>(
storage.files[name].data_file.path(),
storage.files[name].marks[mark_number].offset));
#.Не должно быть пробелов на концах строк.
#. Исходники в кодировке UTF-8.
#.В строковых литералах можно использовать не-ASCII.
#. Нельзя объявлять несколько переменных разных типов в одном объявлении.
:strike:`int x, *y;`
#. c-style cast не используется.
:strike:`std::cerr << (int)c << std::endl;`
..code-block:: cpp
std::cerr << static_cast<int>(c) << std::endl;
#.В классах и структурах, группируйте отдельно методы и отдельно члены, внутри каждой области видимости.
#. Для не очень большого класса/структуры, можно не отделять объявления методов от реализации.
Аналогично для маленьких методов в любых классах/структурах.
Для шаблонных классов/структур, лучше не отделять объявления методов от реализации (так как иначе они всё равно должны быть определены в той же единице трансляции).
#.Не обязательно умещать код по ширине в 80 символов. Можно в 140.
#. Всегда используйте префиксный инкремент/декремент, если постфиксный не нужен.
..code-block:: cpp
for (Names::const_iterator it = column_names.begin(); it != column_names.end(); ++it)
Комментарии
-----------
#. Необходимо обязательно писать комментарии во всех нетривиальных местах.
Это очень важно. При написании комментария, можно успеть понять, что код не нужен вообще, или что всё сделано неверно.
..code-block:: cpp
/** Часть куска памяти, которую можно использовать.
* Например, если internal_buffer - 1MB, а из файла для чтения было загружено в буфер
* только 10 байт, то working_buffer будет иметь размер 10 байт
* (working_buffer.end() будет указывать на позицию сразу после тех 10 байт, которых можно прочитать).
*/
#. Комментарии могут быть сколь угодно подробными.
#. Комментарии пишутся до соответствующего кода. В редких случаях - после, на той же строке.
..code-block:: text
/** Парсит и исполняет запрос.
*/
void executeQuery(
ReadBuffer & istr, /// Откуда читать запрос (а также данные для INSERT-а, если есть)
BlockInputStreamPtr & query_plan, /// Сюда может быть записано описание, как выполнялся запрос
QueryProcessingStage::Enum stage = QueryProcessingStage::Complete); /// До какой стадии выполнять SELECT запрос.
#. Комментарии следует писать только на английском языке.
#. При написании библиотеки, разместите подробный комментарий о том, что это такое, в самом главном заголовочном файле.
#. Нельзя писать комментарии, которые не дают дополнительной информации. В частности, *НЕЛЬЗЯ* писать пустые комментарии.
..code-block:: cpp
/*
* 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/)
#. Нельзя писать мусорные комментарии (автор, дата создания...) в начале каждого файла.
#. Однострочные комментарии начинаются с трёх слешей: ``///``, многострочные - с``/**``. Такие комментарии считаются "документрующими".
Замечание: такие комментарии могут использоваться для генерации документации с помощью Doxygen. Но, фактически, Doxygen не используется, так как для навигации по коду гораздо удобне использовать возможности IDE.
#.В начале и конце многострочного комментария, не должно быть пустых строк (кроме строки, на которой закрывается многострочный комментарий).
#. Для закомментированных кусков кода, используются обычные, не "документирующие" комментарии.
Удаляйте закомментированные куски кода перед коммитом.
#.Не нужно писать нецензурную брань в комментариях.
#.Не нужно писать в комментариях слишком много восклицательных знаков или знаков вопроса, или выделять слишком много слов большими буквами.
:strike:`/// WHAT THE FAIL???`
#.Не нужно составлять из комментариев строки-разделители.
#. Имена классов (структур) - CamelCase с большой буквы. Префиксы кроме I для интерфейсов - не используются.
..code-block:: cpp
class StorageMemory : public IStorage
#. Имена using-ов - также, как классов, либо можно добавить _t на конце.
#. Имена типов - параметров шаблонов: в простых случаях - T; T, U; T1, T2.
В более сложных случаях - либо также, как имена классов, либо можно добавить в начало букву T.
..code-block:: cpp
template <typename TKey, typename TValue>
struct AggregatedStatElement
#. Имена констант - параметров шаблонов: либо также, как имена переменных, либо N - в простом случае.
..code-block:: cpp
template <bool without_www>
struct ExtractDomain
#. Для абстрактных классов (интерфейсов) можно добавить в начало имени букву I.
..code-block:: cpp
class IBlockInputStream
#. Если переменная используется достаточно локально, то можно использовать короткое имя.
В остальных случаях - используйте достаточно подробное имя, описывающее смысл.
..code-block:: cpp
bool info_successfully_loaded = false;
#. define-ы - ALL_CAPS с подчёркиванием. Глобальные константы - тоже.
..code-block:: cpp
#define MAX_SRC_TABLE_NAMES_TO_STORE 1000
#. Имена файлов с кодом называйте по стилю соответственно тому, что в них находится.
Если в файле находится один класс - назовите файл, как класс - в CamelCase.
Если в файле находится одна функция - назовите файл, как функцию - в camelCase.
#. Если имя содержит сокращение, то:
* для имён переменных, всё сокращение пишется маленькими буквами;
``mysql_connection``
:strike:`mySQL_connection`
* для имён классов и функций, сохраняются большие буквы в сокращении.
``MySQLConnection``
:strike:`MySqlConnection`
#. Параметры конструктора, использующиеся сразу же для инициализации соответствующих членов класса, следует назвать также, как и члены класса, добавив подчёркивание в конец.
..code-block:: cpp
FileQueueProcessor(
const std::string & path_,
const std::string & prefix_,
std::shared_ptr<FileHandler> handler_)
: path(path_),
prefix(prefix_),
handler(handler_),
log(&Logger::get("FileQueueProcessor"))
{
}
Также можно называть параметры конструктора так же, как и члены класса (не добавлять подчёркивание), но только если этот параметр не используется в теле конструктора.
#. Именование локальных переменных и членов класса никак не отличается (никакие префиксы не нужны).
``timer``
:strike:`m_timer`
#. Константы в enum-е - CamelCase с большой буквы. Также допустимо ALL_CAPS. Если enum не локален, то используйте enum class.
..code-block:: cpp
enum class CompressionMethod
{
QuickLZ = 0,
LZ4 = 1,
};
#.Все имена - по английски. Транслит с русского использовать нельзя.
:strike:`Stroka`
#. Сокращения (из нескольких букв разных слов) в именах можно использовать только если они являются общепринятыми (если для сокращения можно найти расшифровку в английской википедии или сделав поисковый запрос).
``AST````SQL``
:strike:`NVDH (неведомая х.)`
Сокращения в виде обрезанного слова можно использовать, только если такое сокращение является широко используемым.
Впрочем, сокращения также можно использовать, если расшифровка находится рядом в комментарии.
#. Имена файлов с исходниками на C++ должны иметь расширение только .cpp. Заголовочные файлы - только .h.
Ручное освобождение памяти (delete) можно использовать только в библиотечном коде.
В свою очередь, в библиотечном коде, оператор delete можно использовать только в деструкторах.
В прикладном коде следует делать так, что память освобождается каким-либо объектом, который владеет ей.
Примеры:
* проще всего разместить объект на стеке, или сделать его членом другого класса.
* для большого количества маленьких объектов используйте контейнеры.
* для автоматического освобождения маленького количества объектов, выделенных на куче, используйте shared_ptr/unique_ptr.
#. Управление ресурсами.
Используйте RAII и см. пункт выше.
#. Обработка ошибок.
Используйте исключения. В большинстве случаев, нужно только кидать исключения, а ловить - не нужно (потому что RAII).
В программах offline обработки данных, зачастую, можно не ловить исключения.
В серверах, обрабатывающих пользовательские запросы, как правило, достаточно ловить исключения на самом верху обработчика соединения.
В функциях потока, следует ловить и запоминать все исключения, чтобы выкинуть их в основном потоке после join.
..code-block:: cpp
/// Если вычислений ещё не было - вычислим первый блок синхронно
if (!started)
{
calculate();
started = true;
}
else /// Если вычисления уже идут - подождём результата
pool.wait();
if (exception)
exception->rethrow();
Ни в коем случае не "проглатывайте" исключения без разбора. Ни в коем случае, не превращайте все исключения без разбора в сообщения в логе.
:strike:`catch (...) {}`
Если вам нужно проигнорировать какие-то исключения, то игнорируйте только конкретные, а остальные - кидайте обратно.
..code-block:: cpp
catch (const DB::Exception & e)
{
if (e.code() == ErrorCodes::UNKNOWN_AGGREGATE_FUNCTION)
return nullptr;
else
throw;
}
При использовании функций, использующих коды возврата или errno - проверяйте результат и кидайте исключение.
..code-block:: cpp
if (0 != close(fd))
throwFromErrno("Cannot close file " + file_name, ErrorCodes::CANNOT_CLOSE_FILE);
assert-ы не используются.
#. Типы исключений.
В прикладном коде не требуется использовать сложную иерархию исключений. Желательно, чтобы текст исключения был понятен системному администратору.
#. Исключения, вылетающие из деструкторов.
Использовать не рекомендуется, но допустимо.
Используйте следующие варианты:
* Сделайте функцию (done() или finalize()), которая позволяет заранее выполнить всю работу, в процессе которой может возникнуть исключение. Если эта функция была вызвана, то затем в деструкторе не должно возникать исключений.
* Слишком сложную работу (например, отправку данных по сети) можно вообще не делать в деструкторе, рассчитывая, что пользователь заранее позовёт метод для завершения работы.
* Если в деструкторе возникло исключение, желательно не "проглатывать" его, а вывести информацию в лог (если в этом месте доступен логгер).
*В простых программах, если соответствующие исключения не ловятся, и приводят к завершению работы с записью информации в лог, можно не беспокоиться об исключениях, вылетающих из деструкторов, так как вызов std::terminate (в случае noexcept по-умолчанию в C++11), является приемлимым способом обработки исключения.
#. Отдельные блоки кода.
Внутри одной функции, можно создать отдельный блок кода, для того, чтобы сделать некоторые переменные локальными в нём, и для того, чтобы соответствующие деструкторы были вызваны при выходе из блока.
..code-block:: cpp
Block block = data.in->read();
{
std::lock_guard<std::mutex> lock(mutex);
data.ready = true;
data.block = block;
}
ready_any.set();
#. Многопоточность.
В программах offline обработки данных:
* cначала добейтесь более-менее максимальной производительности на одном процессорном ядре;
* потом можно распараллеливать код, но только если есть необходимость.
В программах - серверах:
* используйте пул потоков для обработки запросов;
* на данный момент, у нас не было задач, в которых была бы необходимость использовать userspace context switching.
Fork для распараллеливания не используется.
#. Синхронизация потоков.
Часто можно сделать так, чтобы отдельные потоки писали данные в разные ячейки памяти (лучше - в разные кэш-линии), и не использовать синхронизацию потоков (кроме joinAll).
Если синхронизация нужна, то в большинстве случаев, достаточно использовать mutex под lock_guard-ом.
В остальных случаях, используйте системные примитивы синхронизации. Не используйте busy wait.
Атомарные операции можно использовать только в простейших случаях.
Не нужно писать самостоятельно lock-free структуры данных, если вы не являетесь экспертом.
#. Ссылки и указатели.
В большинстве случаев, предпочитайте ссылки.
#. const.
Используйте константные ссылки, указатели на константу, const_iterator, константные методы.
Считайте, что const - вариант написания "по-умолчанию", а отсутствие const - только при необходимости.
Для переменных, передающихся по значению, использовать const обычно не имеет смысла.
#. unsigned.
Используйте unsigned, если нужно.
#. Числовые типы.
Используйте типы UInt8, UInt16, UInt32, UInt64, Int8, Int16, Int32, Int64, а также size_t, ssize_t, ptrdiff_t.
Не используйте для чисел типы signed/unsigned long, long long, short; signed char, unsigned char, а также char.
#. Передача аргументов.
Сложные значения передавайте по ссылке (включая std::string).
Если функция захватывает владение объектом, созданным на куче, то сделайте типом аргумента shared_ptr или unique_ptr.
#. Возврат значений.
В большинстве случаев, просто возвращайте значение с помощью return. Не пишите :strike:`return std::move(res)`.
Если внутри функции создаётся объект на куче и отдаётся наружу, то возвращайте shared_ptr или unique_ptr.
В некоторых редких случаях, может потребоваться возвращать значение через аргумент функции. В этом случае, аргументом будет ссылка.
..code-block:: cpp
using AggregateFunctionPtr = std::shared_ptr<IAggregateFunction>;
/** Позволяет создать агрегатную функцию по её имени.
Для прикладного кода отдельный namespace использовать не нужно.
Для маленьких библиотек - не требуется.
Для не совсем маленьких библиотек - поместите всё в namespace.
Внутри библиотеки в .h файле можно использовать namespace detail для деталей реализации, не нужных прикладному коду.
В .cpp файле можно использовать static или анонимный namespace для скрытия символов.
Также, namespace можно использовать для enum, чтобы соответствующие имена не попали во внешний namespace (но лучше использовать enum class).
#. Отложенная инициализация.
Обычно, если для инициализации требуются аргументы, то не пишите конструктор по-умопчанию.
Если потом вам потребовалась отложенная инициализация, то вы можете дописать конструктор по-умолчанию (который создаст объект с некорректным состоянием). Или, для небольшого количества объектов, можно использовать shared_ptr/unique_ptr.
Если класс не предназначен для полиморфного использования, то не нужно делать функции виртуальными зря. Это относится и к деструктору.
#. Кодировки.
Везде используется UTF-8. Используется ``std::string``, ``char *``. Не используется ``std::wstring``, ``wchar_t``.
#. Логгирование.
См. примеры везде в коде.
Перед коммитом, удалите всё бессмысленное и отладочное логгирование, и другие виды отладочного вывода.
Не должно быть логгирования на каждую итерацию внутреннего цикла, даже уровня Trace.
При любом уровне логгирования, логи должно быть возможно читать.
Логгирование следует использовать, в основном, только в прикладном коде.
Сообщения в логе должны быть написаны на английском языке.
Желательно, чтобы лог был понятен системному администратору.
Не нужно писать ругательства в лог.
В логе используется кодировка UTF-8. Изредка можно использовать в логе не-ASCII символы.
#. Ввод-вывод.
Во внутренних циклах (в критичных по производительности участках программы) нельзя использовать iostreams (в том числе, ни в коем случае не используйте stringstream).
Вместо этого используйте библиотеку DB/IO.
#. Дата и время.
См. библиотеку DateLUT.
#. include.
В заголовочном файле используется только ``#pragma once``, а include guard-ы писать не нужно.
#. using.
using namespace не используется.
using что-то конкретное - можно. Лучше локально - внутри класса или функции.
#.Не нужно использовать trailing return type для функций, если в этом нет необходимости.
:strike:`auto f() -> void;`
#.Не нужно объявлять и инициализировать переменные так:
:strike:`auto s = std::string{"Hello"};`
Надо так:
``std::string s = "Hello";``
``std::string s{"Hello"};``
#. Для виртуальных функций, пишите virtual в базовом классе, а в классах-наследниках, пишите override и не пишите virtual.
Неиспользуемые возможности языка C++
------------------------------------
#. Виртуальное наследование не используется.
#. Спецификаторы исключений из C++03 не используются.
#. Function try block не используется, за исключением функции main в тестах.
Платформа
---------
#. Мы пишем некроссплатформенный код (под конкретную платформу).
Хотя, при прочих равных условиях, предпочитается более-менее кроссплатформенный или легко портируемый код.
#. Язык - C++17. Возможно использование расширений GNU при необходимости.
#. Компилятор - gcc. На данный момент (апрель 2017), код собирается версией 6.3. (Также код может быть собран clang 4)
Используется стандартная библиотека от gcc.
#.ОС - Linux Ubuntu, не более старая, чем Precise.
#. Код пишется под процессор с архитектурой x86_64.
Набор инструкций - минимальный поддерживаемый среди наших серверов. Сейчас это - SSE4.2.
#. Используются флаги компиляции ``-Wall -Werror``.
#. Используется статическая линковка со всеми библиотеками кроме тех, которые трудно подключить статически (см. вывод команды ldd).
#. Код разрабатывается и отлаживается с релизными параметрами сборки.
Инструментарий
--------------
#. Хорошая среда разработки - KDevelop.
#. Для отладки используется gdb, valgrind (memcheck), strace, -fsanitize=..., tcmalloc_minimal_debug.
#. Для профилирования используется Linux Perf, valgrind (callgrind), strace -cf.
#. Исходники в Git.
#. Сборка с помощью CMake.
#. Программы выкладываются с помощью deb пакетов.
#. Коммиты в master не должны ломать сборку проекта.
А работоспособность собранных программ гарантируется только для отдельных ревизий.
#. Коммитьте как можно чаще, в том числе и не рабочий код.
Для этого следует использовать бранчи.
Если ваш код в master-е ещё не собирается, перед push-ем - исключите его из сборки;
также вы будете должны его доработать или удалить в течение нескольких дней.
#. Для нетривиальных изменений, используются бранчи. Следует загружать бранчи на сервер.
#. Ненужный код удаляется из исходников.
Библиотеки
----------
#. Используются стандартная библиотека C++14 (допустимо использовать experimental расширения) а также фреймворки boost, Poco.
#. При необходимости, можно использовать любые известные библиотеки, доступные в ОС из пакетов.
Если есть хорошее готовое решение, то оно используется, даже если для этого придётся установить ещё одну библиотеку.
(Но будьте готовы к тому, что иногда вам придётся выкидывать плохие библиотеки из кода.)
#. Если в пакетах нет нужной библиотеки, или её версия достаточно старая, или если она собрана не так, как нужно, то можно использовать библиотеку, устанавливаемую не из пакетов.
#. Если библиотека достаточно маленькая и у неё нет своей системы сборки, то следует включить её файлы в проект, в директорию contrib.
#. Предпочтение всегда отдаётся уже использующимся библиотекам.
Общее
-----
#. Пишите как можно меньше кода.
#. Пробуйте самое простое решение.
#.Не нужно писать код, если вы ещё не знаете, что будет делать ваша программа, и как будет работать её внутренний цикл.
#.В простейших случаях, используйте using вместо классов/структур.
#. Если есть возможность - не пишите конструкторы копирования, операторы присваивания, деструктор (кроме виртуального, если класс содержит хотя бы одну виртуальную функцию), move-конструкторы и move-присваивания. То есть, чтобы соответствущие функции, генерируемые компилятором, работали правильно. Можно использовать default.
#. Приветствуется упрощение и уменьшение объёма кода.
Дополнительно
-------------
#. Явное указание std:: для типов из stddef.h.
Рекомендуется не указывать. То есть, рекомендуется писать size_t вместо std::size_t - потому что это короче.
Но при желании, вы можете всё-таки приписать std:: - такой вариант тоже допустим.
#. Явное указание std:: для функций из стандартной библиотеки C.
Не рекомендуется. То есть, пишите memcpy вместо std::memcpy.
Причина - существуют похожие нестандартные функции, например, memmem. Мы можем использовать и изредка используем эти функции. Эти функции отсутствуют в namespace std.
Если вы везде напишете std::memcpy вместо memcpy, то будет неудобно смотреться memmem без std::.
Тем не менее, указывать std:: тоже допустимо, если так больше нравится.
#. Использование функций из C при наличии аналогов в стандартной библиотеке C++.
Допустимо, если это использование эффективнее.
Для примера, для копирования длинных кусков памяти, используйте memcpy вместо std::copy.
#. Перенос длинных аргументов функций.
Допустимо использовать любой стиль переноса, похожий на приведённые ниже: