Rvalue-ссылки

Используйте rvalue-ссылки только в следующих случаях:

  • Для определения move-конструктора и move-assign оператора
  • Для определения методов, которые оставляют this в неинициализированном/невалидном состоянии.
  • Для поддержки perfect-forwarding в сочетании с std::forward.

Friend

В общем случае разрешено использование “классов-друзей”, с одним условием: класс-друг должен быть объявлен в том же заголовочном файле, что и исходный класс.

Примеры хорошего использования “друзей”:

  • Паттерн “Строитель
  • Юнит-тесты
  • Фабрики

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

Исключения и обработка ошибок

Мы не используем исключения в С++.

Однако если сторонняя библиотека выбрасывает исключения (например, Boost или стандартная библиотека С++), хорошей практикой является их отлавливать.

Примечание

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

Альтернативы

Если функция может вернуть значение, а может и не вернуть (например, оператор [] у вектора), то лучшим решением будет std::optional:

template <typename T>
struct Vector {
  private:
    T* m_values;
    size_t m_size;		
    
  [[nodiscard]] auto get(size_t const index) -> optional<T> {
    if(index >= this->m_size)
      return std::nullopt;
    return this->m_values[index];
  }
};

Если функция может провалиться с каким-либо сообщением об ошибке, то правильнее всего использовать std::expected(C++23)/tl::expected/lf::types::expected:

auto read_file(string_view path) -> expected<string, string> {
  using lf::Err;
  if(auto const p = std::filesystem::path(path); not exists(p))
    return Err("failed to open file at {}", path);
  auto const contents = /* ... */;
  return contents;
}

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

class File {
  private:
    File();
  public:
    [[nodiscard]] static auto open() -> expected<File, string>;
};
absl::Status

Допускается также использование класса Status из открытой библиотеки Abseil от Google. Например:

#include <absl/status>
 
class File {
  private:
    File();
  public:
    [[nodiscard]] static auto open() -> absl::StatusOr<File> {
      if(/* ... */)
        return absl::NotFoundError("bad file path");
      return absl::OkStatus(/* ... */);
    }
}

Подробнее можно прочитать здесь: ссылка.

Noexcept

Функции, обозначенные как noexcept будут завершать работу всей программы с помощью std::terminate, если любое исключение покинет тело этой функции.

Например:

auto divide(int a, int b) noexcept {
	if(b == 0)
		throw std::invalid_argument("division by zero");
	return a / b;
}

auto const a = divide(1, 0); // приводит к завершению программы
  • Обозначайте функцию как noexcept тогда, когда это уместно.
  • Никогда не обозначайте как noexcept те функции, которые могут при определенных обстоятельствах выбросить исключения.
  • Не обозначайте функцию как noexcept, если в дальнейшем это может измениться и функция является частью публичного API.

Если функция выбрасывает исключения, то ее следует помечать как noexcept(false). Это даст понимание тому, кто использует ваше API, что ему следует произвести отлавливание исключения.

RTTI

RTTI (Run-Time Type Information) предоставляет возможность узнать тип объекта во времени выполнения.

Старайтесь избегать использования RTTI. Исключением является dynamic_cast и получения имени типа для логирования.

Если у вас где-либо встречается использование typeid в условиях/циклах, то логика вашего кода сильно нарушена и ее стоит пересмотреть. Пример плохого кода:

/* так делать не нужно! */
if(typeid(*ptr) == typeid(int))
  // ...
else if(typeid(*ptr) == typeid(float))
  // ...
else
  // ...

Приведение типов

Используйте касты в стиле C++.

Замечание

Никогда не используйте касты в стиле С (если только вы не пишете на чистом C). Пример плохого кода: int a = (int)M_PI. Исключением для этого правила является приведение к void.

Замечание

Также не используйте касты через конструкторы (например, int a = int(M_PI)).

  • Используйте static_cast<T> для приведения простых типов или для приведения указателя на родительский класс к указателю на подкласс.
  • Используйте const_cast<T>, чтобы убрать квалификатор const.
  • Используйте reinterpret_cast<T> для небезопасных приведений указателей, но делайте это с осторожностью.
  • Используйте dynamic_cast<T> для приведений между родительскими и дочерними классами.
  • Используйте std::bit_cast<T> для прямой побитовой конверсии типов равного размера (например, uint64_t в double).
  • Используйте qobject_cast<T> там, где это необходимо. Stackoverflow
  • Используйте boost::lexical_cast<T> для приведения типов в строку и обратно. Boost.Org

Потоки

К потокам относятся стандартные потоковые классы стандартной библиотеки С++ (например, std::cout, std::cerr, std::ifstream, std::stringstream).

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

ОперацияПотокQTПрочие реализацииПредпочтениеПримечание
Чтение файлаstd::ifstreamQFileFILE*, std::FILE*✅Потоки
Запись в файлstd::ofstreamQFileFILE*, std::FILE*✅Потоки
Вывод в консольstd::cout, std::cerr, std::clogqDebug, qWarning, qInfo, qCriticalprintf, perr, библиотеки для логирования⛔Библиотеки для логгированияИспользуйте spdlog для логирования в консоль или файл.
Приведение к строкеstd::stringstreamQStringboost::lexical_castboost::lexical_cast, если доступен, или стандартные потоки

Если вы определяете оператор << для какого-либо из потоков (std::ostream, QDebug), убедитесь, что он выводит человеко-читаемую форму вашего типа.

Примечание

Хорошим тоном будет определить не только перегрузку для вашего потока, но и fmt::formatter для функции fmt::format. Подробнее: ссылка

Префиксный и постфиксный инкремент/декремент

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

for(auto i = 0; i < 100; i++) /* плохо */
 
for(auto i = 0; i < 100; ++i) /* хорошо */

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

Используйте спецификатор const всегда, где это возможно: в функциях, параметрах и переменных. Хорошей практикой будет всегда объявлять переменные как const, и убирать квалификатор позже, если это необходимо.

Некоторые выражения с квалификатором const на самом деле могут являться constexpr. Хорошей практикой будет преобразовывать такие выражения в constexpr.

Если параметр функции является константным, то в заголовочном файле его нужно объявить без спецификатора const, а в файле реализации - со спецификатором:

/* meow.h */
class Cat {
	auto meow(int a) const -> void;
}
 
/* meow.cc */
auto Cat::meow(int const a) const -> void { /* ... */ }

Где помещать спецификатор?

Существует две равнозначные формы записи квалификатора const: 1

auto const i = 1;

2

const auto i = 1;

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

Выражения времени компиляции

В С++ выражения, выполняющиеся на этапе компиляции, обозначаются следующими ключевыми словами:

  • constexpr - функция/выражение может потенциально быть выполнено на этапе компиляции, но это не гарантируется.
  • consteval - функция может быть вызвана только на этапе компиляции и только на нем.
  • constinit - гарантирует константную инициализацию для неконстантной переменной. Использование данных ключевых слово активно поощряется и приводит к ускорению работы приложения. Примеры хорошего использования:
constexpr-переменные
constexpr auto PI = 3.14f;
constexpr auto buffer = std::array<uint8_t, 1024>{ /* ... */ };
constexpr-функции и конструкторы
constexpr auto add(int const a, int const b) -> int { return a + b; }
struct source_location {
  constexpr explicit source_location(string_view file, string_view line);
}
consteval-функции
constexpr auto factorial(uint64_t n) -> uint64_t { 
  return n < 2 ? 1 : n * factorial(n - 1); 
}
 
consteval auto combination(uint64_t m, uint64_t n) -> uint64_t {
  return factorial(n) / factorial(m) / factorial(n - m);
}
 
// проверка на этапе компиляции
static_assert(factorial(6) == 720);
static_assert(combination(4, 8) == 70);
 
auto main() -> int {
  std::cout << factorial(6) << std::endl;      // OK
  std::cout << combination(4, 8) << std::endl; // Ошибка
}

Про наиболее распостраненное использование constinit можно прочитать здесь: Статические и локальные для потока переменные

Целочисленные типы

Использование следующих ключевых слов является плохой практикой:

  • int
  • char
  • unsigned
  • long
  • short Все вышеперечисленные типы являются платформозависимыми, и их размер и знаковость отличается на разных платформах и компиляторах.
Альтернативы

Простая альтернатива - использование заголовочного файла <cstdint>, который предоставляет следующие типы:

int8_t
int16_t
int32_t
int64_t
uint8_t
uint16_t
uint32_t
uint64_t
uintptr_t
intptr_t
ptrdiff_t
size_t

В библиотеке Leaf также есть более короткие псевдонимы для данных типов:

namespace leaf::types {
  using u8 = uint8_t;       ///< 8-bit беззнаковое целое
  using u16 = uint16_t;     ///< 16-bit беззнаковое целое
  using u32 = uint32_t;     ///< 32-bit беззнаковое целое
  using u64 = uint64_t;     ///< 64-bit беззнаковое целое
  using i8 = int8_t;        ///< 8-bit знаковое целое
  using i16 = int16_t;      ///< 16-bit знаковое целое
  using i32 = int32_t;      ///< 32-bit знаковое целое
  using i64 = int64_t;      ///< 64-bit знаковое целое
  using usize = size_t;     ///< Беззнаковое целое размером с указатель на `void`
  using isize = intptr_t;   ///< Знаковое целое размером с указатель на `void`
  using f32 = float;        ///< 32-bit число с плавающей точкой (`float`)
  using f64 = double;       ///< 64-bit число с плавающей точкой (`double`)
  using f128 = long double; ///< 128-bit число с плавающей точкой (`long double`)
}
32- и 64- битные архитектуры

Ваш код должен работать одинаково и на 32-разрядных, и на 64-разрядных архитектурах. Общие советы для достижения этой цели:

  • Используйте size_t для индексирования по коллекциям
  • Используйте intptr_t/uintptr_t в случаях, когда требуется целое число размером с регистр процессора (sizeof(void))
  • Используйте ptrdiff_t для обозначения разности между двумя указателями (e.g. std::distance)

Макросы

Предупреждение

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

  • inline-функций
  • constexpr/consteval-функций и переменных
  • Шаблонных функций и классов

Исключением может являться:

  • Макрос для конверсии в строку (stringify-макрос)
  • Макросы для определения платформозависимого кода, который будет раскрываться в разные значения в зависимости от компилятора/системы. Таким образом можно заменять некоторые ключевые слова.
  • Функциональные макросы, цель которых - генерация кода (объявление функций, классов и т.д), которые невозможно заменить шаблонами.

Если вы все же решили объявить макрос, то следуйте следующим правилам:

  • Никогда не определяйте макросы в заголовочных файлах публичного API
  • Старайтесь определять макрос там, где вы его используйте, затем выполняйте #undef
  • Давайте макросам уникальные имена, которые гарантированно не приведут к коллизиям.

Альтернативы

Константы
#define PI 3.14              // 😒 плохо!
 
constexpr auto PI = 3.14;    // 😊 хорошо!

Эта секция будет пополняться в дальнейшем.

Нулевые типы

  • Используйте nullptr для обозначения нулевых указателей
  • Используйте '\0' для обозначения null-terminatorа в строках
  • Используйте std::nullopt для обозначения пустого std::optional<T>

Замечание

Никогда не используйте NULL или 0 для обозначения нулевого указателя.

Auto

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

Если требуется явный тип, то его можно объявить так:

auto vec = std::vector<int>();
 
auto const object = Cat();

Если auto ассоциирует себя не с той переменной, которую вы ожидаете (например, в случае с std::vector<bool>), то используйте явное приведение или конструктор:

auto const flags = std::vector<bool>{true, false, true, false};
 
auto const first_flag = static_cast<bool>(flags[0]);
auto const second_flag = bool(flags[1]);

Для итерации по контейнерам также используйте ключевое слово auto:

for(auto num : numbers) // для итерации по значению
 
for(auto& word : words) // для итерации по изменяемой ссылке
 
for(auto const& v : values) // для итерации по константной ссылке

Аналогичным образом используйте семантику ссылок для объявления переменной ссылочного типа:

auto vec = std::vector<int>{1, 2, 3, 4};
 
auto value = vec.at(0);          // значение
auto& mut_ref = vec.at(1);       // изменяемая ссылка
auto const& ref = vec.at(2);     // константная ссылка
auto* ptr = &vec.at(3);          // указатель

Нестандартные расширения языка

См. Нестандартные расширения компилятора

Далее: Соглашение по именованию