mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2024-11-23 16:12:01 +00:00
8535c5ce40
Co-authored-by: olgarev <56617294+olgarev@users.noreply.github.com>
905 lines
49 KiB
Markdown
905 lines
49 KiB
Markdown
---
|
||
toc_priority: 68
|
||
toc_title: "Как писать код на C++"
|
||
---
|
||
|
||
|
||
# Как писать код на C++ {#kak-pisat-kod-na-c}
|
||
|
||
## Общее {#obshchee}
|
||
|
||
**1.** Этот текст носит рекомендательный характер.
|
||
|
||
**2.** Если вы редактируете код, то имеет смысл писать так, как уже написано.
|
||
|
||
**3.** Стиль нужен для единообразия. Единообразие нужно, чтобы было проще (удобнее) читать код. А также, чтобы было легче осуществлять поиск по коду.
|
||
|
||
**4.** Многие правила продиктованы не какими либо разумными соображениями, а сложившейся практикой.
|
||
|
||
## Форматирование {#formatirovanie}
|
||
|
||
**1.** Большую часть форматирования сделает автоматически `clang-format`.
|
||
|
||
**2.** Отступы — 4 пробела. Настройте среду разработки так, чтобы таб добавлял четыре пробела.
|
||
|
||
**3.** Открывающая и закрывающие фигурные скобки на отдельной строке.
|
||
|
||
``` cpp
|
||
inline void readBoolText(bool & x, ReadBuffer & buf)
|
||
{
|
||
char tmp = '0';
|
||
readChar(tmp, buf);
|
||
x = tmp != '0';
|
||
}
|
||
```
|
||
|
||
**4.** Если всё тело функции — один `statement`, то его можно разместить на одной строке. При этом, вокруг фигурных скобок ставятся пробелы (кроме пробела на конце строки).
|
||
|
||
``` cpp
|
||
inline size_t mask() const { return buf_size() - 1; }
|
||
inline size_t place(HashValue x) const { return x & mask(); }
|
||
```
|
||
|
||
**5.** Для функций. Пробелы вокруг скобок не ставятся.
|
||
|
||
``` cpp
|
||
void reinsert(const Value & x)
|
||
```
|
||
|
||
``` cpp
|
||
memcpy(&buf[place_value], &x, sizeof(x));
|
||
```
|
||
|
||
**6.** В выражениях `if`, `for`, `while` и т.д. перед открывающей скобкой ставится пробел (в отличие от вызовов функций).
|
||
|
||
``` cpp
|
||
for (size_t i = 0; i < rows; i += storage.index_granularity)
|
||
```
|
||
|
||
**7.** Вокруг бинарных операторов (`+`, `-`, `*`, `/`, `%`, …), а также тернарного оператора `?:` ставятся пробелы.
|
||
|
||
``` cpp
|
||
UInt16 year = (s[0] - '0') * 1000 + (s[1] - '0') * 100 + (s[2] - '0') * 10 + (s[3] - '0');
|
||
UInt8 month = (s[5] - '0') * 10 + (s[6] - '0');
|
||
UInt8 day = (s[8] - '0') * 10 + (s[9] - '0');
|
||
```
|
||
|
||
**8.** Если ставится перенос строки, то оператор пишется на новой строке, и перед ним увеличивается отступ.
|
||
|
||
``` cpp
|
||
if (elapsed_ns)
|
||
message << " ("
|
||
<< rows_read_on_server * 1000000000 / elapsed_ns << " rows/s., "
|
||
<< bytes_read_on_server * 1000.0 / elapsed_ns << " MB/s.) ";
|
||
```
|
||
|
||
**9.** Внутри строки можно, выполнять выравнивание с помощью пробелов.
|
||
|
||
``` cpp
|
||
dst.ClickLogID = click.LogID;
|
||
dst.ClickEventID = click.EventID;
|
||
dst.ClickGoodEvent = click.GoodEvent;
|
||
```
|
||
|
||
**10.** Вокруг операторов `.`, `->` не ставятся пробелы.
|
||
|
||
При необходимости, оператор может быть перенесён на новую строку. В этом случае, перед ним увеличивается отступ.
|
||
|
||
**11.** Унарные операторы `--`, `++`, `*`, `&`, … не отделяются от аргумента пробелом.
|
||
|
||
**12.** После запятой ставится пробел, а перед — нет. Аналогично для точки с запятой внутри выражения `for`.
|
||
|
||
**13.** Оператор `[]` не отделяется пробелами.
|
||
|
||
**14.** В выражении `template <...>`, между `template` и `<` ставится пробел, а после `<` и до `>` не ставится.
|
||
|
||
``` cpp
|
||
template <typename TKey, typename TValue>
|
||
struct AggregatedStatElement
|
||
{}
|
||
```
|
||
|
||
**15.** В классах и структурах, `public`, `private`, `protected` пишется на том же уровне, что и `class/struct`, а остальной код с отступом.
|
||
|
||
``` cpp
|
||
template <typename T>
|
||
class MultiVersion
|
||
{
|
||
public:
|
||
/// Version of object for usage. shared_ptr manage lifetime of version.
|
||
using Version = std::shared_ptr<const T>;
|
||
...
|
||
}
|
||
```
|
||
|
||
**16.** Если на весь файл один `namespace` и кроме него ничего существенного нет, то отступ внутри `namespace` не нужен.
|
||
|
||
**17.** Если блок для выражения `if`, `for`, `while`, … состоит из одного `statement`, то фигурные скобки не обязательны. Вместо этого поместите `statement` на отдельную строку. Это правило справедливо и для вложенных `if`, `for`, `while`, …
|
||
|
||
Если внутренний `statement` содержит фигурные скобки или `else`, то внешний блок следует писать в фигурных скобках.
|
||
|
||
``` cpp
|
||
/// Finish write.
|
||
for (auto & stream : streams)
|
||
stream.second->finalize();
|
||
```
|
||
|
||
**18.** Не должно быть пробелов на концах строк.
|
||
|
||
**19.** Исходники в кодировке UTF-8.
|
||
|
||
**20.** В строковых литералах можно использовать не-ASCII.
|
||
|
||
``` cpp
|
||
<< ", " << (timer.elapsed() / chunks_stats.hits) << " μsec/hit.";
|
||
```
|
||
|
||
**21.** Не пишите несколько выражений в одной строке.
|
||
|
||
**22.** Внутри функций группируйте блоки кода, отделяя их не более, чем одной пустой строкой.
|
||
|
||
**23.** Функции, классы, и т. п. отделяются друг от друга одной или двумя пустыми строками.
|
||
|
||
**24.** `const` (относящийся к значению) пишется до имени типа.
|
||
|
||
``` cpp
|
||
//correct
|
||
const char * pos
|
||
const std::string & s
|
||
//incorrect
|
||
char const * pos
|
||
```
|
||
|
||
**25.** При объявлении указателя или ссылки, символы `*` и `&` отделяются пробелами с обеих сторон.
|
||
|
||
``` cpp
|
||
//correct
|
||
const char * pos
|
||
//incorrect
|
||
const char* pos
|
||
const char *pos
|
||
```
|
||
|
||
**26.** При использовании шаблонных типов, пишите `using` (кроме, возможно, простейших случаев).
|
||
|
||
То есть, параметры шаблона указываются только в `using` и затем не повторяются в коде.
|
||
|
||
`using` может быть объявлен локально, например, внутри функции.
|
||
|
||
``` cpp
|
||
//correct
|
||
using FileStreams = std::map<std::string, std::shared_ptr<Stream>>;
|
||
FileStreams streams;
|
||
//incorrect
|
||
std::map<std::string, std::shared_ptr<Stream>> streams;
|
||
```
|
||
|
||
**27.** Нельзя объявлять несколько переменных разных типов в одном выражении.
|
||
|
||
``` cpp
|
||
//incorrect
|
||
int x, *y;
|
||
```
|
||
|
||
**28.** C-style cast не используется.
|
||
|
||
``` cpp
|
||
//incorrect
|
||
std::cerr << (int)c <<; std::endl;
|
||
//correct
|
||
std::cerr << static_cast<int>(c) << std::endl;
|
||
```
|
||
|
||
**29.** В классах и структурах, группируйте отдельно методы и отдельно члены, внутри каждой области видимости.
|
||
|
||
**30.** Для не очень большого класса/структуры, можно не отделять объявления методов от реализации.
|
||
|
||
Аналогично для маленьких методов в любых классах/структурах.
|
||
|
||
Для шаблонных классов/структур, лучше не отделять объявления методов от реализации (так как иначе они всё равно должны быть определены в той же единице трансляции).
|
||
|
||
**31.** Не обязательно умещать код по ширине в 80 символов. Можно в 140.
|
||
|
||
**32.** Всегда используйте префиксный инкремент/декремент, если постфиксный не нужен.
|
||
|
||
``` cpp
|
||
for (Names::const_iterator it = column_names.begin(); it != column_names.end(); ++it)
|
||
```
|
||
|
||
## Комментарии {#kommentarii}
|
||
|
||
**1.** Необходимо обязательно писать комментарии во всех нетривиальных местах.
|
||
|
||
Это очень важно. При написании комментария, можно успеть понять, что код не нужен вообще, или что всё сделано неверно.
|
||
|
||
``` cpp
|
||
/** Part of piece of memory, that can be used.
|
||
* For example, if internal_buffer is 1MB, and there was only 10 bytes loaded to buffer from file for reading,
|
||
* then working_buffer will have size of only 10 bytes
|
||
* (working_buffer.end() will point to position right after those 10 bytes available for read).
|
||
*/
|
||
```
|
||
|
||
**2.** Комментарии могут быть сколь угодно подробными.
|
||
|
||
**3.** Комментарии пишутся до соответствующего кода. В редких случаях после, на той же строке.
|
||
|
||
``` cpp
|
||
/** Parses and executes the query.
|
||
*/
|
||
void executeQuery(
|
||
ReadBuffer & istr, /// Where to read the query from (and data for INSERT, if applicable)
|
||
WriteBuffer & ostr, /// Where to write the result
|
||
Context & context, /// DB, tables, data types, engines, functions, aggregate functions...
|
||
BlockInputStreamPtr & query_plan, /// Here could be written the description on how query was executed
|
||
QueryProcessingStage::Enum stage = QueryProcessingStage::Complete /// Up to which stage process the SELECT query
|
||
)
|
||
```
|
||
|
||
**4.** Комментарии следует писать только на английском языке.
|
||
|
||
**5.** При написании библиотеки, разместите подробный комментарий о том, что это такое, в самом главном заголовочном файле.
|
||
|
||
**6.** Нельзя писать комментарии, которые не дают дополнительной информации. В частности, нельзя писать пустые комментарии вроде этого:
|
||
|
||
``` 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/.
|
||
|
||
**7.** Нельзя писать мусорные комментарии (автор, дата создания…) в начале каждого файла.
|
||
|
||
**8.** Однострочные комментарии начинаются с трёх слешей: `///` , многострочные с `/**`. Такие комментарии считаются «документирующими».
|
||
|
||
Замечание: такие комментарии могут использоваться для генерации документации с помощью Doxygen. Но, фактически, Doxygen не используется, так как для навигации по коду гораздо удобнее использовать возможности IDE.
|
||
|
||
**9.** В начале и конце многострочного комментария, не должно быть пустых строк (кроме строки, на которой закрывается многострочный комментарий).
|
||
|
||
**10.** Для закомментированных кусков кода, используются обычные, не «документирующие» комментарии.
|
||
|
||
**11.** Удаляйте закомментированные куски кода перед коммитом.
|
||
|
||
**12.** Не нужно писать нецензурную брань в комментариях или коде.
|
||
|
||
**13.** Не пишите прописными буквами. Не используйте излишнее количество знаков препинания.
|
||
|
||
``` cpp
|
||
/// WHAT THE FAIL???
|
||
```
|
||
|
||
**14.** Не составляйте из комментариев строки-разделители.
|
||
|
||
``` cpp
|
||
///******************************************************
|
||
```
|
||
|
||
**15.** Не нужно писать в комментарии диалог (лучше сказать устно).
|
||
|
||
``` cpp
|
||
/// Why did you do this stuff?
|
||
```
|
||
|
||
**16.** Не нужно писать комментарий в конце блока о том, что представлял собой этот блок.
|
||
|
||
``` cpp
|
||
/// for
|
||
```
|
||
|
||
## Имена {#imena}
|
||
|
||
**1.** В именах переменных и членов класса используйте маленькие буквами с подчёркиванием.
|
||
|
||
``` cpp
|
||
size_t max_block_size;
|
||
```
|
||
|
||
**2.** Имена функций (методов) camelCase с маленькой буквы.
|
||
|
||
``` cpp
|
||
std::string getName() const override { return "Memory"; }
|
||
```
|
||
|
||
**3.** Имена классов (структур) - CamelCase с большой буквы. Префиксы кроме I для интерфейсов - не используются.
|
||
|
||
``` cpp
|
||
class StorageMemory : public IStorage
|
||
```
|
||
|
||
**4.** `using` называются также, как классы, либо с `_t` на конце.
|
||
|
||
**5.** Имена типов — параметров шаблонов: в простых случаях - `T`; `T`, `U`; `T1`, `T2`.
|
||
|
||
В более сложных случаях - либо также, как имена классов, либо можно добавить в начало букву `T`.
|
||
|
||
``` cpp
|
||
template <typename TKey, typename TValue>
|
||
struct AggregatedStatElement
|
||
```
|
||
|
||
**6.** Имена констант — параметров шаблонов: либо также, как имена переменных, либо `N` в простом случае.
|
||
|
||
``` cpp
|
||
template <bool without_www>
|
||
struct ExtractDomain
|
||
```
|
||
|
||
**7.** Для абстрактных классов (интерфейсов) можно добавить в начало имени букву `I`.
|
||
|
||
``` cpp
|
||
class IBlockInputStream
|
||
```
|
||
|
||
**8.** Если переменная используется достаточно локально, то можно использовать короткое имя.
|
||
|
||
В остальных случаях используйте имя, описывающее смысл.
|
||
|
||
``` cpp
|
||
bool info_successfully_loaded = false;
|
||
```
|
||
|
||
**9.** В именах `define` и глобальных констант используется ALL_CAPS с подчёркиванием.
|
||
|
||
``` cpp
|
||
#define MAX_SRC_TABLE_NAMES_TO_STORE 1000
|
||
```
|
||
|
||
**10.** Имена файлов с кодом называйте по стилю соответственно тому, что в них находится.
|
||
|
||
Если в файле находится один класс, назовите файл, как класс (CamelCase).
|
||
|
||
Если в файле находится одна функция, назовите файл, как функцию (camelCase).
|
||
|
||
**11.** Если имя содержит сокращение, то:
|
||
|
||
- для имён переменных, всё сокращение пишется маленькими буквами `mysql_connection` (не `mySQL_connection`).
|
||
- для имён классов и функций, сохраняются большие буквы в сокращении `MySQLConnection` (не `MySqlConnection`).
|
||
|
||
**12.** Параметры конструктора, использующиеся сразу же для инициализации соответствующих членов класса, следует назвать также, как и члены класса, добавив подчёркивание в конец.
|
||
|
||
``` 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"))
|
||
{
|
||
}
|
||
```
|
||
|
||
Также можно называть параметры конструктора так же, как и члены класса (не добавлять подчёркивание), но только если этот параметр не используется в теле конструктора.
|
||
|
||
**13.** Именование локальных переменных и членов класса никак не отличается (никакие префиксы не нужны).
|
||
|
||
``` cpp
|
||
timer (not m_timer)
|
||
```
|
||
|
||
**14.** Константы в `enum` — CamelCase с большой буквы. Также допустим ALL_CAPS. Если `enum` не локален, то используйте `enum class`.
|
||
|
||
``` cpp
|
||
enum class CompressionMethod
|
||
{
|
||
QuickLZ = 0,
|
||
LZ4 = 1,
|
||
};
|
||
```
|
||
|
||
**15.** Все имена - по-английски. Транслит с русского использовать нельзя.
|
||
|
||
``` text
|
||
не Stroka
|
||
```
|
||
|
||
**16.** Сокращения (из нескольких букв разных слов) в именах можно использовать только если они являются общепринятыми (если для сокращения можно найти расшифровку в английской википедии или сделав поисковый запрос).
|
||
|
||
``` text
|
||
`AST`, `SQL`.
|
||
|
||
Не `NVDH` (что-то неведомое)
|
||
```
|
||
|
||
Сокращения в виде обрезанного слова можно использовать, только если такое сокращение является широко используемым.
|
||
|
||
Впрочем, сокращения также можно использовать, если расшифровка находится рядом в комментарии.
|
||
|
||
**17.** Имена файлов с исходниками на C++ должны иметь расширение только `.cpp`. Заголовочные файлы - только `.h`.
|
||
|
||
## Как писать код {#kak-pisat-kod}
|
||
|
||
**1.** Управление памятью.
|
||
|
||
Ручное освобождение памяти (`delete`) можно использовать только в библиотечном коде.
|
||
|
||
В свою очередь, в библиотечном коде, оператор `delete` можно использовать только в деструкторах.
|
||
|
||
В прикладном коде следует делать так, что память освобождается каким-либо объектом, который владеет ей.
|
||
|
||
Примеры:
|
||
|
||
- проще всего разместить объект на стеке, или сделать его членом другого класса.
|
||
- для большого количества маленьких объектов используйте контейнеры.
|
||
- для автоматического освобождения маленького количества объектов, выделенных на куче, используйте `shared_ptr/unique_ptr`.
|
||
|
||
**2.** Управление ресурсами.
|
||
|
||
Используйте `RAII` и см. пункт выше.
|
||
|
||
**3.** Обработка ошибок.
|
||
|
||
Используйте исключения. В большинстве случаев, нужно только кидать исключения, а ловить - не нужно (потому что `RAII`).
|
||
|
||
В программах офлайн обработки данных, зачастую, можно не ловить исключения.
|
||
|
||
В серверах, обрабатывающих пользовательские запросы, как правило, достаточно ловить исключения на самом верху обработчика соединения.
|
||
|
||
В функциях потока, следует ловить и запоминать все исключения, чтобы выкинуть их в основном потоке после `join`.
|
||
|
||
``` cpp
|
||
/// Если вычислений ещё не было - вычислим первый блок синхронно
|
||
if (!started)
|
||
{
|
||
calculate();
|
||
started = true;
|
||
}
|
||
else /// Если вычисления уже идут - подождём результата
|
||
pool.wait();
|
||
|
||
if (exception)
|
||
exception->rethrow();
|
||
```
|
||
|
||
Ни в коем случае не «проглатывайте» исключения без разбора. Ни в коем случае, не превращайте все исключения без разбора в сообщения в логе.
|
||
|
||
``` cpp
|
||
//Not correct
|
||
catch (...) {}
|
||
```
|
||
|
||
Если вам нужно проигнорировать какие-то исключения, то игнорируйте только конкретные, а остальные кидайте обратно.
|
||
|
||
``` cpp
|
||
catch (const DB::Exception & e)
|
||
{
|
||
if (e.code() == ErrorCodes::UNKNOWN_AGGREGATE_FUNCTION)
|
||
return nullptr;
|
||
else
|
||
throw;
|
||
}
|
||
```
|
||
|
||
При использовании функций, использующих коды возврата или `errno`, проверяйте результат и кидайте исключение.
|
||
|
||
``` cpp
|
||
if (0 != close(fd))
|
||
throwFromErrno("Cannot close file " + file_name, ErrorCodes::CANNOT_CLOSE_FILE);
|
||
```
|
||
|
||
`assert` не используются.
|
||
|
||
**4.** Типы исключений.
|
||
|
||
В прикладном коде не требуется использовать сложную иерархию исключений. Желательно, чтобы текст исключения был понятен системному администратору.
|
||
|
||
**5.** Исключения, вылетающие из деструкторов.
|
||
|
||
Использовать не рекомендуется, но допустимо.
|
||
|
||
Используйте следующие варианты:
|
||
|
||
- Сделайте функцию (`done()` или `finalize()`), которая позволяет заранее выполнить всю работу, в процессе которой может возникнуть исключение. Если эта функция была вызвана, то затем в деструкторе не должно возникать исключений.
|
||
- Слишком сложную работу (например, отправку данных по сети) можно вообще не делать в деструкторе, рассчитывая, что пользователь заранее позовёт метод для завершения работы.
|
||
- Если в деструкторе возникло исключение, желательно не «проглатывать» его, а вывести информацию в лог (если в этом месте доступен логгер).
|
||
- В простых программах, если соответствующие исключения не ловятся, и приводят к завершению работы с записью информации в лог, можно не беспокоиться об исключениях, вылетающих из деструкторов, так как вызов `std::terminate` (в случае `noexcept` по умолчанию в C++11), является приемлемым способом обработки исключения.
|
||
|
||
**6.** Отдельные блоки кода.
|
||
|
||
Внутри одной функции, можно создать отдельный блок кода, для того, чтобы сделать некоторые переменные локальными в нём, и для того, чтобы соответствующие деструкторы были вызваны при выходе из блока.
|
||
|
||
``` cpp
|
||
Block block = data.in->read();
|
||
|
||
{
|
||
std::lock_guard<std::mutex> lock(mutex);
|
||
data.ready = true;
|
||
data.block = block;
|
||
}
|
||
|
||
ready_any.set();
|
||
```
|
||
|
||
**7.** Многопоточность.
|
||
|
||
В программах офлайн обработки данных:
|
||
|
||
- cначала добейтесь более-менее максимальной производительности на одном процессорном ядре, потом можно распараллеливать код, но только если есть необходимость.
|
||
|
||
В программах - серверах:
|
||
|
||
- используйте пул потоков для обработки запросов. На данный момент, у нас не было задач, в которых была бы необходимость использовать userspace context switching.
|
||
|
||
Fork для распараллеливания не используется.
|
||
|
||
**8.** Синхронизация потоков.
|
||
|
||
Часто можно сделать так, чтобы отдельные потоки писали данные в разные ячейки памяти (лучше в разные кэш-линии), и не использовать синхронизацию потоков (кроме `joinAll`).
|
||
|
||
Если синхронизация нужна, то в большинстве случаев, достаточно использовать mutex под `lock_guard`.
|
||
|
||
В остальных случаях, используйте системные примитивы синхронизации. Не используйте busy wait.
|
||
|
||
Атомарные операции можно использовать только в простейших случаях.
|
||
|
||
Не нужно писать самостоятельно lock-free структуры данных, если вы не являетесь экспертом.
|
||
|
||
**9.** Ссылки и указатели.
|
||
|
||
В большинстве случаев, предпочитайте ссылки.
|
||
|
||
**10.** const.
|
||
|
||
Используйте константные ссылки, указатели на константу, `const_iterator`, константные методы.
|
||
|
||
Считайте, что `const` — вариант написания «по умолчанию», а отсутствие `const` только при необходимости.
|
||
|
||
Для переменных, передающихся по значению, использовать `const` обычно не имеет смысла.
|
||
|
||
**11.** unsigned.
|
||
|
||
Используйте `unsigned`, если нужно.
|
||
|
||
**12.** Числовые типы.
|
||
|
||
Используйте типы `UInt8`, `UInt16`, `UInt32`, `UInt64`, `Int8`, `Int16`, `Int32`, `Int64`, а также `size_t`, `ssize_t`, `ptrdiff_t`.
|
||
|
||
Не используйте для чисел типы `signed/unsigned long`, `long long`, `short`, `signed/unsigned char`, `char`.
|
||
|
||
**13.** Передача аргументов.
|
||
|
||
Сложные значения передавайте по ссылке (включая `std::string`).
|
||
|
||
Если функция захватывает владение объектом, созданным на куче, то сделайте типом аргумента `shared_ptr` или `unique_ptr`.
|
||
|
||
**14.** Возврат значений.
|
||
|
||
В большинстве случаев, просто возвращайте значение с помощью `return`. Не пишите `return std::move(res)`.
|
||
|
||
Если внутри функции создаётся объект на куче и отдаётся наружу, то возвращайте `shared_ptr` или `unique_ptr`.
|
||
|
||
В некоторых редких случаях, может потребоваться возвращать значение через аргумент функции. В этом случае, аргументом будет ссылка.
|
||
|
||
``` cpp
|
||
using AggregateFunctionPtr = std::shared_ptr<IAggregateFunction>;
|
||
|
||
/** Позволяет создать агрегатную функцию по её имени.
|
||
*/
|
||
class AggregateFunctionFactory
|
||
{
|
||
public:
|
||
AggregateFunctionFactory();
|
||
AggregateFunctionPtr get(const String & name, const DataTypes & argument_types) const;
|
||
```
|
||
|
||
**15.** namespace.
|
||
|
||
Для прикладного кода отдельный `namespace` использовать не нужно.
|
||
|
||
Для маленьких библиотек - не требуется.
|
||
|
||
Для не совсем маленьких библиотек - поместите всё в `namespace`.
|
||
|
||
Внутри библиотеки в `.h` файле можно использовать `namespace detail` для деталей реализации, не нужных прикладному коду.
|
||
|
||
В `.cpp` файле можно использовать `static` или анонимный namespace для скрытия символов.
|
||
|
||
Также, `namespace` можно использовать для `enum`, чтобы соответствующие имена не попали во внешний `namespace` (но лучше использовать `enum class`).
|
||
|
||
**16.** Отложенная инициализация.
|
||
|
||
Обычно, если для инициализации требуются аргументы, то не пишите конструктор по умолчанию.
|
||
|
||
Если потом вам потребовалась отложенная инициализация, то вы можете дописать конструктор по умолчанию (который создаст объект с некорректным состоянием). Или, для небольшого количества объектов, можно использовать `shared_ptr/unique_ptr`.
|
||
|
||
``` cpp
|
||
Loader(DB::Connection * connection_, const std::string & query, size_t max_block_size_);
|
||
|
||
/// Для отложенной инициализации
|
||
Loader() {}
|
||
```
|
||
|
||
**17.** Виртуальные функции.
|
||
|
||
Если класс не предназначен для полиморфного использования, то не нужно делать функции виртуальными зря. Это относится и к деструктору.
|
||
|
||
**18.** Кодировки.
|
||
|
||
Везде используется UTF-8. Используется `std::string`, `char *`. Не используется `std::wstring`, `wchar_t`.
|
||
|
||
**19.** Логирование.
|
||
|
||
См. примеры везде в коде.
|
||
|
||
Перед коммитом, удалите всё бессмысленное и отладочное логирование, и другие виды отладочного вывода.
|
||
|
||
Не должно быть логирования на каждую итерацию внутреннего цикла, даже уровня Trace.
|
||
|
||
При любом уровне логирования, логи должно быть возможно читать.
|
||
|
||
Логирование следует использовать, в основном, только в прикладном коде.
|
||
|
||
Сообщения в логе должны быть написаны на английском языке.
|
||
|
||
Желательно, чтобы лог был понятен системному администратору.
|
||
|
||
Не нужно писать ругательства в лог.
|
||
|
||
В логе используется кодировка UTF-8. Изредка можно использовать в логе не-ASCII символы.
|
||
|
||
**20.** Ввод-вывод.
|
||
|
||
Во внутренних циклах (в критичных по производительности участках программы) нельзя использовать `iostreams` (в том числе, ни в коем случае не используйте `stringstream`).
|
||
|
||
Вместо этого используйте библиотеку `DB/IO`.
|
||
|
||
**21.** Дата и время.
|
||
|
||
См. библиотеку `DateLUT`.
|
||
|
||
**22.** include.
|
||
|
||
В заголовочном файле используется только `#pragma once`, а include guards писать не нужно.
|
||
|
||
**23.** using.
|
||
|
||
`using namespace` не используется. Можно использовать `using` что-то конкретное. Лучше локально, внутри класса или функции.
|
||
|
||
**24.** Не нужно использовать `trailing return type` для функций, если в этом нет необходимости.
|
||
|
||
``` cpp
|
||
auto f() -> void
|
||
```
|
||
|
||
**25.** Объявление и инициализация переменных.
|
||
|
||
``` cpp
|
||
//right way
|
||
std::string s = "Hello";
|
||
std::string s{"Hello"};
|
||
|
||
//wrong way
|
||
auto s = std::string{"Hello"};
|
||
```
|
||
|
||
**26.** Для виртуальных функций, пишите `virtual` в базовом классе, а в классах-наследниках, пишите `override` и не пишите `virtual`.
|
||
|
||
## Неиспользуемые возможности языка C++ {#neispolzuemye-vozmozhnosti-iazyka-c}
|
||
|
||
**2.** Спецификаторы исключений из C++03 не используются.
|
||
|
||
## Сообщения об ошибках {#error-messages}
|
||
|
||
Сообщения об ошибках -- это часть пользовательского интерфейса программы, предназначенная для того, чтобы позволить пользователю:
|
||
|
||
* замечать ошибочные ситуации,
|
||
* понимать их смысл и причины,
|
||
* устранять эти ситуации.
|
||
|
||
Форма и содержание сообщений об ошибках должны способствовать достижению этих целей.
|
||
|
||
Есть два основных вида ошибок:
|
||
|
||
* пользовательская или системная ошибка,
|
||
* внутренняя программная ошибка.
|
||
|
||
### Пользовательская ошибка {#error-messages-user-error}
|
||
|
||
Такая ошибка вызвана действиями пользователя (неверный синтаксис запроса) или конфигурацией внешних систем (кончилось место на диске). Предполагается, что пользователь может устранить её самостоятельно. Для этого в сообщении об ошибке должна содержаться следующая информация:
|
||
|
||
* что произошло. Это должно объясняться в пользовательских терминах (`Function pow() is not supported for data type UInt128`), а не загадочными конструкциями из кода (`runtime overload resolution failed in DB::BinaryOperationBuilder<FunctionAdaptor<pow>::Impl, UInt128, Int8>::kaboongleFastPath()`).
|
||
* почему/где/когда -- любой контекст, который помогает отладить проблему. Представьте, как бы её отлаживали вы (программировать и пользоваться отладчиком нельзя).
|
||
* что можно предпринять для устранения ошибки. Здесь можно перечислить типичные причины проблемы, настройки, влияющие на это поведение, и так далее.
|
||
|
||
Пример нормального сообщения:
|
||
```
|
||
No alias for subquery or table function in JOIN (set joined_subquery_requires_alias=0 to disable restriction).
|
||
While processing '(SELECT 2 AS a)'.
|
||
```
|
||
Сказано что не хватает алиаса, показано, для какой части запроса, и предложена настройка, позволяющая ослабить это требование.
|
||
|
||
Пример катастрофически плохого сообщения:
|
||
```
|
||
The dictionary is configured incorrectly.
|
||
```
|
||
Из него не понятно:
|
||
|
||
- какой словарь?
|
||
- в чём ошибка конфигурации?
|
||
|
||
Что может сделать пользователь в такой ситуации: применять внешние отладочные инструменты, спрашивать совета на форумах, гадать на кофейной гуще, и, конечно же, ненавидеть софт, который над ним так издевается. Не нужно издеваться над пользователями, это плохой UX.
|
||
|
||
|
||
### Внутренняя программная ошибка {#error-messages-internal-error}
|
||
|
||
Такая ошибка вызвана нарушением внутренних инвариантов программы: например, внутренняя функция вызвана с неверными параметрами, не совпадают размеры колонок в блоке, произошло разыменование нулевого указателя, и так далее. Сигналы типа `SIGSEGV` относятся к этой же категории.
|
||
|
||
Появление такой ошибки всегда свидетельствует о наличии бага в программе. Пользователь не может исправить такую ошибку самостоятельно, и должен сообщить о ней разработчикам.
|
||
|
||
Есть два основных варианта проверки на такие ошибки:
|
||
|
||
* Исключение с кодом `LOGICAL_ERROR`. Его можно использовать для важных проверок, которые делаются в том числе в релизной сборке.
|
||
* `assert`. Такие условия не проверяются в релизной сборке, можно использовать для тяжёлых и опциональных проверок.
|
||
|
||
Пример сообщения, у которого должен быть код `LOGICAL_ERROR`:
|
||
`Block header is inconsistent with Chunk in ICompicatedProcessor::munge(). It is a bug!`
|
||
По каким признакам можно заметить, что здесь говорится о внутренней программной ошибке?
|
||
|
||
* в сообщении упоминаются внутренние сущности из кода,
|
||
* в сообщении написано it's a bug,
|
||
* непосредственные действия пользователя не могут исправить эту ошибку. Мы ожидаем, что пользователь зарепортит её как баг, и будем исправлять в коде.
|
||
|
||
### Как выбрать код ошибки? {#error-messages-choose}
|
||
|
||
Код ошибки предназначен для автоматической обработки некоторых видов ошибок, подобно кодам HTTP. SQL стандартизирует некоторые коды, но на деле ClickHouse не всегда соответствует этим стандартам. Лучше всего выбрать существующий код из `ErrorCodes.cpp`, который больше всего подходит по смыслу. Можно использовать общие коды типа `BAD_ARGUMENTS` или `TYPE_MISMATCH`. Заводить новый код нужно, только если вы чётко понимаете, что вам нужна специальная автоматическая обработка конкретно этой ошибки на клиенте. Для внутренних программных ошибок используется код `LOGICAL_ERROR`.
|
||
|
||
### Как добавить новое сообщение об ошибке? {#error-messages-add}
|
||
|
||
Когда добавляете сообщение об ошибке:
|
||
|
||
1. Опишите, что произошло, в пользовательских терминах, а не кусками кода.
|
||
2. Добавьте максимум контекста (с чем произошло, когда, почему, и т.д.).
|
||
3. Добавьте типичные причины.
|
||
4. Добавьте варианты исправления (настройки, ссылки на документацию).
|
||
5. Вообразите дальнейшие действия пользователя. Ваше сообщение должно помочь ему решить проблему без использования отладочных инструментов и без чужой помощи.
|
||
6. Если сообщение об ошибке не формулируется в пользовательских терминах, и действия пользователя не могут исправить проблему -- это внутренняя программная ошибка, используйте код LOGICAL_ERROR или assert.
|
||
|
||
## Платформа {#platforma}
|
||
|
||
**1.** Мы пишем код под конкретные платформы.
|
||
|
||
Хотя, при прочих равных условиях, предпочитается более-менее кроссплатформенный или легко портируемый код.
|
||
|
||
**2.** Язык - C++20 (см. список доступных [C++20 фич](https://en.cppreference.com/w/cpp/compiler_support#C.2B.2B20_features)).
|
||
|
||
**3.** Компилятор - `clang`. На данный момент (апрель 2021), код собирается версией 11. (Также код может быть собран `gcc` версии 10, но такая сборка не тестируется и непригодна для продакшена).
|
||
|
||
Используется стандартная библиотека (реализация `libc++`).
|
||
|
||
**4.** ОС - Linux, Mac OS X или FreeBSD.
|
||
|
||
**5.** Код пишется под процессоры с архитектурой x86_64, AArch64 и ppc64le.
|
||
|
||
**6.** Используются флаги компиляции `-Wall -Wextra -Werror` и `-Weverything` с некоторыми исключениями.
|
||
|
||
**7.** Используется статическая линковка со всеми библиотеками кроме libc.
|
||
|
||
## Инструментарий {#instrumentarii}
|
||
|
||
**1.** Хорошая среда разработки - KDevelop.
|
||
|
||
**2.** Для отладки используется `gdb`, `valgrind` (`memcheck`), `strace`, `-fsanitize=...`, `tcmalloc_minimal_debug`.
|
||
|
||
**3.** Для профилирования используется `Linux Perf`, `valgrind` (`callgrind`), `strace -cf`.
|
||
|
||
**4.** Исходники в Git.
|
||
|
||
**5.** Сборка с помощью `CMake`.
|
||
|
||
**6.** Программы выкладываются с помощью `deb` пакетов.
|
||
|
||
**7.** Коммиты в master не должны ломать сборку проекта.
|
||
|
||
А работоспособность собранных программ гарантируется только для отдельных ревизий.
|
||
|
||
**8.** Коммитьте как можно чаще, в том числе и нерабочий код.
|
||
|
||
Для этого следует использовать бранчи.
|
||
|
||
Если ваш код в ветке `master` ещё не собирается, исключите его из сборки перед `push`, также вы будете должны его доработать или удалить в течение нескольких дней.
|
||
|
||
**9.** Для нетривиальных изменений, используются бранчи. Следует загружать бранчи на сервер.
|
||
|
||
**10.** Ненужный код удаляется из исходников.
|
||
|
||
## Библиотеки {#libraries}
|
||
|
||
**1.** Используются стандартные библиотеки C++20 (допустимо использовать экспериментальные расширения), а также фреймворки `boost`, `Poco`.
|
||
|
||
**2.** Библиотеки должны быть расположены в виде исходников в директории `contrib` и собираться вместе с ClickHouse. Не разрешено использовать библиотеки, доступные в пакетах ОС, или любые другие способы установки библиотек в систему. Подробнее смотрите раздел [Рекомендации по добавлению сторонних библиотек и поддержанию в них пользовательских изменений](contrib.md#adding-third-party-libraries).
|
||
|
||
**3.** Предпочтение отдаётся уже использующимся библиотекам.
|
||
|
||
## Общее {#obshchee-1}
|
||
|
||
**1.** Пишите как можно меньше кода.
|
||
|
||
**2.** Пробуйте самое простое решение.
|
||
|
||
**3.** Не нужно писать код, если вы ещё не знаете, что будет делать ваша программа, и как будет работать её внутренний цикл.
|
||
|
||
**4.** В простейших случаях, используйте `using` вместо классов/структур.
|
||
|
||
**5.** Если есть возможность - не пишите конструкторы копирования, операторы присваивания, деструктор (кроме виртуального, если класс содержит хотя бы одну виртуальную функцию), move-конструкторы и move-присваивания. То есть, чтобы соответствущие функции, генерируемые компилятором, работали правильно. Можно использовать `default`.
|
||
|
||
**6.** Приветствуется упрощение и уменьшение объёма кода.
|
||
|
||
## Дополнительно {#dopolnitelno}
|
||
|
||
**1.** Явное указание `std::` для типов из `stddef.h`.
|
||
|
||
Рекомендуется не указывать. То есть, рекомендуется писать `size_t` вместо `std::size_t`, это короче.
|
||
|
||
При желании, можно дописать `std::`, этот вариант допустим.
|
||
|
||
**2.** Явное указание `std::` для функций из стандартной библиотеки C.
|
||
|
||
Не рекомендуется. То есть, пишите `memcpy` вместо `std::memcpy`.
|
||
|
||
Причина - существуют похожие нестандартные функции, например, `memmem`. Мы можем использовать и изредка используем эти функции. Эти функции отсутствуют в `namespace std`.
|
||
|
||
Если вы везде напишете `std::memcpy` вместо `memcpy`, то будет неудобно смотреться `memmem` без `std::`.
|
||
|
||
Тем не менее, указывать `std::` тоже допустимо, если так больше нравится.
|
||
|
||
**3.** Использование функций из C при наличии аналогов в стандартной библиотеке C++.
|
||
|
||
Допустимо, если это использование эффективнее.
|
||
|
||
Для примера, для копирования длинных кусков памяти, используйте `memcpy` вместо `std::copy`.
|
||
|
||
**4.** Перенос длинных аргументов функций.
|
||
|
||
Допустимо использовать любой стиль переноса, похожий на приведённые ниже:
|
||
|
||
``` cpp
|
||
function(
|
||
T1 x1,
|
||
T2 x2)
|
||
```
|
||
|
||
``` cpp
|
||
function(
|
||
size_t left, size_t right,
|
||
const & RangesInDataParts ranges,
|
||
size_t limit)
|
||
```
|
||
|
||
``` cpp
|
||
function(size_t left, size_t right,
|
||
const & RangesInDataParts ranges,
|
||
size_t limit)
|
||
```
|
||
|
||
``` cpp
|
||
function(size_t left, size_t right,
|
||
const & RangesInDataParts ranges,
|
||
size_t limit)
|
||
```
|
||
|
||
``` cpp
|
||
function(
|
||
size_t left,
|
||
size_t right,
|
||
const & RangesInDataParts ranges,
|
||
size_t limit)
|
||
```
|