Правило трёх (C++ программирование)

Материал из Википедии — свободной энциклопедии
Перейти к: навигация, поиск

Правило трёх (также известное как «Закон Большой Тройки» или «Большая Тройка») — правило в C++, гласящее, что если класс или структура определяет один из следующих методов, то они должны явным образом определить все три метода[1]:

Эти три метода являются особыми членами-функциями, автоматически создаваемыми компилятором в случае отсутствия их явного объявления программистом. Если один из них должен быть определен программистом, то это означает, что версия, сгенерированная компилятором, не удовлетворяет потребностям класса в одном случае и, вероятно, не удовлетворит в остальных случаях.

Поправка к этому правилу заключается в том, что если используется RAII (от англ. Resource Acquisition Is Initialization), то используемый деструктор может остаться неопределённым (иногда упоминается как «Закон Большой Двойки»)[2].

Так как неявно определённые конструкторы и операторы присваивания просто копируют все члены-данные класса[3], определение явных конструкторов копирования и операторов присваивания копированием необходимо в случаях, когда класс инкапсулирует сложные структуры данных или может поддерживать эксклюзивный доступ к ресурсам. А также в случаях, когда класс содержит константные данные или ссылки.

Правило пяти[править | править вики-текст]

С выходом одиннадцатого стандарта правило расширилось и теперь называется правило пяти. Теперь при реализации конструктора необходимо реализовать:

  • Деструктор
  • Конструктор копирования
  • Оператор присваивания копированием
  • Конструктор перемещения
  • Оператор присваивания перемещением[4]

Пример правила пяти:

 1 #include <cstring>
 2 class RFive
 3 {
 4     char* cstring;
 5  public:
 6     RFive(const char* arg)  // Конструктор со списком инициализации и телом
 7     : cstring(new char[std::strlen(arg)+1])
 8     {
 9         std::strcpy(cstring, arg);
10     }
11     ~RFive()  // Деструктор
12     {
13         delete[] cstring;
14     }
15     RFive(const RFive& other) // Конструктор копирования
16     {
17         cstring = new char[std::strlen(other.cstring) + 1];
18         std::strcpy(cstring, other.cstring);
19     }
20     RFive(RFive&& other) : cstring(other.cstring) // Конструктор перемещения
21     {
22         other.cstring = nullptr;
23     }
24     RFive& operator=(const RFive& other) // Оператор присваивания копированием (copy assignment)
25     {
26         char* tmp_cstring = new char[std::strlen(other.cstring) + 1];
27         std::strcpy(tmp_cstring, other.cstring);
28         delete[] cstring;
29         cstring = tmp_cstring;
30         return *this;
31     }
32     RFive& operator=(RFive&& other) // Оператор присваивания перемещением (move assignment)
33     {
34         delete[] cstring;
35         cstring = other.cstring;
36         other.cstring = nullptr;
37         return *this;
38     }
39 //  Также можно заменить оба оператора присваивания следующим оператором
40 //  RFive& operator=(RFive other)
41 //  {
42 //      std::swap(cstring, other.cstring);
43 //      return *this;
44 //  }
45 };

Идиома копирования и обмена[править | править вики-текст]

Всегда стоит избегать дублирования одного и того же кода, так как при изменении или исправлении одного участка, придётся не забыть исправить остальные. Идиома копирования и обмена (англ. copy and swap idiom) позволяет этого избежать, используя повторно код конструктора копирования. Так для класса RFive придётся создать дружественную функцию swap и реализовать оператор присванивания копированием и перемещением через неё. Более того, при такой реализации отпадает нужда в проверке на самоприсваивание.

 1 #include <cstring>
 2 class RFive
 3 {
 4     // остальной код
 5     RFive& operator=(const RFive& other) // Оператор присваивания копированием (copy assignment)
 6     {
 7         Rfive tmp(other);
 8         swap (*this, tmp);
 9         return *this;
10     }
11     RFive& operator=(RFive&& other) // Оператор присваивания перемещением (move assignment)
12     {
13         swap (*this, other);
14         return *this;
15     }
16     friend void swap (RFive& l, RFive& r)
17     {
18         using std::swap;
19         swap (l.cstring , r.cstring);
20     }
21     // остальной код
22 };

Также уместно будет для операторов присваивания сделать возвращаемое значение константной ссылкой: const RFive& operator=(const RFive& other);. Дополнительный const не позволит нам писать запутанный код, как, например, такой: (a=b=c).foo();.

Правило ноля[править | править вики-текст]

Мартин Фернандес предложил также правило ноля.[5] По этому правилу не стоит определять ни одну из пяти функций самому; надо поручить их создание компилятору(присвоить им значение = default;). Для владения ресурсами вместо простых указателей стоит использовать специальные классы-обёртки, такие как std::unique_ptr и std::shared_ptr.[6]

Ссылки[править | править вики-текст]

  1. Stroustrup Bjarne. The C++ Programming Language. — 3. — Addison-Wesley. — P. 283-4. — 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. — P. 271.
  4. Move Assignment Operator. Проверено 22 декабря 2014.
  5. Rule of Zero. Flaming Dangerzone. Проверено 29 июля 2015.
  6. Куликов Александр. Правило 3, 5, 0 Хабрахабр.