C++
Правило три, пять и ноль
Поиск…
Правило пяти
C ++ 11 вводит две новые специальные функции-члены: конструктор перемещения и оператор назначения перемещения. По всем тем же причинам, по которым вы хотите следовать правилу 3 в C ++ 03, вы обычно хотите следовать правилу 5 в C ++ 11: если класс требует ОДИН из пяти специальных функций-членов и если семантика перемещения желательны, то это, скорее всего, требует ВСЕ ПЯТЬ из них.
Обратите внимание, однако, что несоблюдение правила пяти обычно не считается ошибкой, но упущена возможность оптимизации, если до сих пор соблюдается правило трех. Если в случае, если компилятор обычно использует один оператор, если оператор-конструктор перемещения или оператор перемещения не доступны, вместо этого он будет использовать семантику копирования, что приведет к менее эффективной операции из-за ненужных операций копирования. Если семантика перемещения не нужна для класса, тогда нет необходимости объявлять конструктор перемещения или оператор присваивания.
Тот же пример, что и для правила трех:
class Person
{
char* name;
int age;
public:
// Destructor
~Person() { delete [] name; }
// Implement Copy Semantics
Person(Person const& other)
: name(new char[std::strlen(other.name) + 1])
, age(other.age)
{
std::strcpy(name, other.name);
}
Person &operator=(Person const& other)
{
// Use copy and swap idiom to implement assignment.
Person copy(other);
swap(*this, copy);
return *this;
}
// Implement Move Semantics
// Note: It is usually best to mark move operators as noexcept
// This allows certain optimizations in the standard library
// when the class is used in a container.
Person(Person&& that) noexcept
: name(nullptr) // Set the state so we know it is undefined
, age(0)
{
swap(*this, that);
}
Person& operator=(Person&& that) noexcept
{
swap(*this, that);
return *this;
}
friend void swap(Person& lhs, Person& rhs) noexcept
{
std::swap(lhs.name, rhs.name);
std::swap(lhs.age, rhs.age);
}
};
В качестве альтернативы оператор копирования и перемещения может быть заменен одним оператором присваивания, который принимает экземпляр по значению вместо ссылки или ссылки на rvalue, чтобы облегчить использование идиомы копирования и свопинга.
Person& operator=(Person copy)
{
swap(*this, copy);
return *this;
}
Переход от правила трех к правилу пяти имеет важное значение для служебных целей, но в большинстве случаев это не является строго необходимым. Добавление конструктора копирования и оператора присваивания гарантирует, что перемещение этого типа не будет утечкой памяти (в этом случае перемещение-построение просто вернется к копированию), но будет выполнять копии, которые, по-видимому, не ожидали вызывающий.
Правило нуля
Мы можем объединить принципы Правил пяти и RAII, чтобы получить более компактный интерфейс: Правило нуля: любой ресурс, который нужно управлять, должен быть в своем собственном типе. Этот тип должен следовать правилу 5, но всем пользователям этого ресурса не нужно писать ни одну из пяти специальных функций-членов, и они могут просто по default
все из них.
Используя класс Person
представленный в примере правила 3 , мы можем создать объект управления ресурсами для cstrings
:
class cstring {
private:
char* p;
public:
~cstring() { delete [] p; }
cstring(cstring const& );
cstring(cstring&& );
cstring& operator=(cstring const& );
cstring& operator=(cstring&& );
/* other members as appropriate */
};
И как только это отдельно, наш класс Person
становится намного проще:
class Person {
cstring name;
int arg;
public:
~Person() = default;
Person(Person const& ) = default;
Person(Person&& ) = default;
Person& operator=(Person const& ) = default;
Person& operator=(Person&& ) = default;
/* other members as appropriate */
};
Особых членов в Person
даже не нужно заявлять явно; компилятор по умолчанию или удалит их соответствующим образом на основе содержимого Person
. Следовательно, следующее правило также является примером правила нуля.
struct Person {
cstring name;
int arg;
};
Если cstring
должен быть типом только для перемещения, с оператором-конструктором / присваиванием delete
d copy, то Person
автоматически будет также перемещаться.
Термин «правило нуля» был введен Р. Мартиньо Фернандесом
Правило трех
Правило трех утверждает, что если тип когда-либо должен иметь пользовательский конструктор копирования, оператор присваивания копии или деструктор, тогда он должен иметь все три .
Причиной этого правила является то, что для управления этим ресурсом необходим класс, который требует, чтобы любой из трех управлял некоторым ресурсом (файловые дескрипторы, динамически распределенная память и т. Д.), И все три. Функции копирования связаны с тем, как ресурс копируется между объектами, а деструктор уничтожает ресурс в соответствии с принципами RAII .
Рассмотрим тип, управляющий строковым ресурсом:
class Person
{
char* name;
int age;
public:
Person(char const* new_name, int new_age)
: name(new char[std::strlen(new_name) + 1])
, age(new_age)
{
std::strcpy(name, new_name);
}
~Person() {
delete [] name;
}
};
Поскольку name
было назначено в конструкторе, деструктор освобождает его, чтобы избежать утечки памяти. Но что произойдет, если такой объект будет скопирован?
int main()
{
Person p1("foo", 11);
Person p2 = p1;
}
Сначала будет построено p1
. Тогда p2
будет скопирован из p1
. Тем не менее, созданный C ++ конструктор копии будет копировать каждый компонент типа as-is. Это означает, что p1.name
и p2.name
указывают на одну и ту же строку.
Когда main
концы, будут вызваны деструкторы. p2
первый деструктор p2
; он удалит строку. Тогда будет вызван деструктор p1
. Однако строка уже удалена . Вызов delete
в уже удаленной памяти дает неопределенное поведение.
Чтобы этого избежать, необходимо предоставить подходящий конструктор копирования. Один из подходов заключается в реализации системы подсчета ссылок, где разные экземпляры Person
имеют одни и те же строковые данные. Каждый раз, когда выполняется копия, счетчик общих ссылок увеличивается. Деструктор затем уменьшает счетчик ссылок, освобождая только память, если счетчик равен нулю.
Или мы можем реализовать семантику значений и глубокое копирование :
Person(Person const& other)
: name(new char[std::strlen(other.name) + 1])
, age(other.age)
{
std::strcpy(name, other.name);
}
Person &operator=(Person const& other)
{
// Use copy and swap idiom to implement assignment
Person copy(other);
swap(copy); // assume swap() exchanges contents of *this and copy
return *this;
}
Реализация оператора присваивания копии осложняется необходимостью выпуска существующего буфера. Метод copy and swap создает временный объект, который содержит новый буфер. Перестановка содержимого *this
и copy
дает право собственности на copy
исходного буфера. Уничтожение copy
, когда функция возвращает, освобождает буфер, ранее принадлежавший *this
.
Защита от самостоятельного назначения
При написании оператора присваивания копии очень важно, чтобы он мог работать в случае самонаведения. То есть, это должно позволить:
SomeType t = ...;
t = t;
Самоназвание обычно не происходит столь очевидным образом. Обычно это происходит через обходной маршрут через различные системы кода, где местоположение назначения просто имеет два указателя на Person
или ссылки и не имеет представления, что они являются одним и тем же объектом.
Любой оператор копирования, который вы пишете, должен учесть это.
Типичный способ сделать это - обернуть всю логику назначения в таком состоянии, как это:
SomeType &operator=(const SomeType &other)
{
if(this != &other)
{
//Do assignment logic.
}
return *this;
}
Примечание. Важно подумать о самоопределении и убедиться, что ваш код ведет себя правильно, когда это происходит. Однако самоназвание является очень редким явлением, и оптимизация для предотвращения его может фактически пессимизировать нормальный случай. Так как нормальный случай гораздо более распространен, пессимизация для самостоятельного назначения может значительно снизить эффективность кода (поэтому будьте осторожны с ним).
Например, обычным методом реализации оператора присваивания является copy and swap idiom
. Обычная реализация этой методики не утруждает себя проверкой на самоназвание (хотя самозадача является дорогостоящей, потому что делается копия). Причина в том, что пессимизация нормального случая оказалась намного более дорогостоящей (как это происходит чаще).
Операторы переадресации также должны быть защищены от самонаправления. Однако логика для многих таких операторов основана на std::swap
, которая может обрабатывать std::swap
из / в одну и ту же память. Поэтому, если ваша логика назначения перемещения - это не что иное, как серия операций свопинга, тогда вам не нужна защита самоназвания.
Если это не так, вы должны принять аналогичные меры, как указано выше.