Правило трёх (C++)
Правило трёх (также известное как «Закон Большой Тройки» или «Большая Тройка») — правило в C++, гласящее, что если класс или структура определяет один из следующих методов, то они должны явным образом определить все три метода[1]:
Эти три метода являются особыми членами-функциями, автоматически создаваемыми компилятором в случае отсутствия их явного объявления программистом. Если один из них должен быть определен программистом, то это означает, что версия, сгенерированная компилятором, не удовлетворяет потребностям класса в одном случае и, вероятно, не удовлетворит в остальных случаях.
Поправка к этому правилу заключается в том, что если используется RAII (от англ. Resource Acquisition Is Initialization), то используемый деструктор может остаться неопределённым (иногда упоминается как «Закон Большой Двойки»)[2].
Так как неявно определённые конструкторы и операторы присваивания просто копируют все члены-данные класса[3], определение явных конструкторов копирования и операторов присваивания копированием необходимо в случаях, когда класс инкапсулирует сложные структуры данных или может поддерживать эксклюзивный доступ к ресурсам. А также в случаях, когда класс содержит константные данные или ссылки.
Правило пяти
С выходом одиннадцатого стандарта правило расширилось и теперь называется правило пяти. Теперь при реализации конструктора необходимо реализовать:
- Деструктор
- Конструктор копирования
- Оператор присваивания копированием
- Конструктор перемещения
- Оператор присваивания перемещением[4]
Пример правила пяти:
#include <cstring>
class RFive
{
private:
char* cstring;
public:
// Конструктор со списком инициализации и телом
RFive(const char* arg)
: cstring(new char[std::strlen(arg)+1])
{
std::strcpy(cstring, arg);
}
// Деструктор
~RFive()
{
delete[] cstring;
}
// Конструктор копирования
RFive(const RFive& other)
{
cstring = new char[std::strlen(other.cstring) + 1];
std::strcpy(cstring, other.cstring);
}
// Конструктор перемещения, noexcept - для оптимизации при использовании стандартных контейнеров
RFive(RFive&& other) noexcept
{
cstring = other.cstring;
other.cstring = nullptr;
}
// Оператор присваивания копированием (copy assignment)
RFive& operator=(const RFive& other)
{
if (this == &other)
return *this;
char* tmp_cstring = new char[std::strlen(other.cstring) + 1];
std::strcpy(tmp_cstring, other.cstring);
delete[] cstring;
cstring = tmp_cstring;
return *this;
}
// Оператор присваивания перемещением (move assignment)
RFive& operator=(RFive&& other) noexcept
{
if (this == &other)
return *this;
delete[] cstring;
cstring = other.cstring;
other.cstring = nullptr;
return *this;
}
// Также можно заменить оба оператора присваивания следующим оператором
// RFive& operator=(RFive other)
// {
// std::swap(cstring, other.cstring);
// return *this;
// }
};
Идиома копирования и обмена
Всегда стоит избегать дублирования одного и того же кода, так как при изменении или исправлении одного участка придётся не забыть исправить остальные. Идиома копирования и обмена (англ. copy and swap idiom) позволяет этого избежать, используя повторно код конструктора копирования, так для класса RFive придётся создать дружественную функцию swap и реализовать оператор присваивания копированием и перемещением через неё. Более того, при такой реализации отпадает нужда в проверке на самоприсваивание.
#include <cstring>
class RFive
{
// остальной код
RFive& operator=(const RFive& other) // Оператор присваивания копированием (copy assignment)
{
Rfive tmp(other);
swap (*this, tmp);
return *this;
}
RFive& operator=(RFive&& other) // Оператор присваивания перемещением (move assignment)
{
swap (*this, other);
return *this;
}
friend void swap (RFive& l, RFive& r)
{
using std::swap;
swap (l.cstring , r.cstring);
}
// остальной код
};
Также уместно будет для операторов присваивания сделать возвращаемое значение константной ссылкой: const RFive& operator=(const RFive& other);
. Дополнительный const не позволит нам писать запутанный код, как, например, такой: (a=b=c).foo();
.
Правило ноля
Мартин Фернандес предложил также правило ноля.[5]
По этому правилу не стоит определять ни одну из пяти функций самому; надо поручить их создание компилятору(присвоить им значение = default;
). Для владения ресурсами вместо простых указателей стоит использовать специальные классы-обёртки, такие как std::unique_ptr
и std::shared_ptr
.[6]
Ссылки
- Bjarne Stroustrup. The C++ Programming Language (неопр.). — 3. — Addison-Wesley, 2000. — С. 283—284. — ISBN 978-0201700732.
- Karlsson, Bjorn; Wilson, Matthew. The Law of the Big Two . The C++ Source. Artima (1 октября 2004). Дата обращения: 22 января 2008. Архивировано 17 марта 2012 года.
- The C++ Programming Language (неопр.). — С. 271.
- Move Assignment Operator . En.CPPReference.com. Дата обращения: 22 декабря 2014.
- Rule of Zero . Flaming Dangerzone. Дата обращения: 29 июля 2015.
- Куликов Александр. Правило 3, 5, 0 Хабрахабр .