Правило трёх (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]

Примечания

[править | править код]
  1. Bjarne Stroustrup. The C++ Programming Language (неопр.). — 3. — Addison-Wesley, 2000. — С. 283—284. — ISBN 978-0201700732.
  2. Karlsson, Bjorn; Wilson, Matthew.: . The Law of the Big Two. The C++ Source. Artima (1 октября 2004). Дата обращения: 22 января 2008. Архивировано 17 марта 2012 года.
  3. The C++ Programming Language (неопр.). — С. 271.
  4. Move Assignment Operator. En.CPPReference.com. Дата обращения: 22 декабря 2014. Архивировано 23 декабря 2014 года.
  5. Rule of Zero. Flaming Dangerzone. Дата обращения: 29 июля 2015. Архивировано 29 августа 2015 года.
  6. Куликов Александр. Правило 3, 5, 0 Хабрахабр. Дата обращения: 14 февраля 2016. Архивировано 22 февраля 2016 года.