Что такое правило трех?

8

c++,copy-constructor,assignment-operator,c++-faq,rule-of-three,

C ++, копировать-конструктор, назначение-оператор, C ++ - чаво, правило трое,

Ответов: 1873


1540 принят

Введение

C ++ обрабатывает переменные пользовательских типов со значениями семантики . Это означает, что объекты неявно копируются в разных контекстах, и мы должны понимать, что на самом деле означает «копирование объекта».

Рассмотрим простой пример:

person

(Если вы озадачены частью, это называется списком инициализаторов членов .)// 1. copy constructor person(const person& that) : name(that.name), age(that.age) { } // 2. copy assignment operator person& operator=(const person& that) { name = that.name; age = that.age; return *this; } // 3. destructor ~person() { }

Специальные функции участника

Что означает копирование nameобъекта? ageФункция показывает два различных сценария копирования. Инициализация personвыполняется конструктором копирования . Его задача - построить новый объект, основанный на состоянии существующего объекта. Назначение personвыполняется оператором присваивания копии . Его работа, как правило, немного сложнее, потому что целевой объект уже находится в некотором правильном состоянии, с которым нужно иметь дело.

Поскольку мы сами не объявили ни конструктор копирования, ни оператор присваивания (или деструктор), они неявно определены для нас. Цитата из стандарта:

Конструктор копии [...] и оператор присваивания копий, [...] и деструктор являются специальными функциями-членами. [ Примечание : реализация неявно объявляет эти функции-члены для некоторых типов классов, когда программа явно не объявляет их. Реализация будет неявно определять их, если они используются. [...] end note ] [n3126.pdf раздел 12 A§1]

По умолчанию копирование объекта означает копирование его элементов:

Неявно определенный конструктор копирования для неединичного класса X выполняет поэтапную копию своих подобъектов. [n3126.pdf раздел 12.8 A§16]

Неявно определенный оператор присваивания копии для неединичного класса X выполняет поэтапное присвоение копии своих подобъектов. [n3126.pdf раздел 12.8 A§30]

Неявные определения

Неявно определенные специальные функции-члены std::stringвыглядят так:

person

Постепенное копирование - это именно то, что мы хотим в этом случае: и копируем, поэтому получаем автономный независимый объект. Неявно определенный деструктор всегда пуст. Это также прекрасно в этом случае, поскольку мы не получили никаких ресурсов в конструкторе. Деструкторы участников неявно вызываются после завершения деструктора:class person { char* name; int age; public: // the constructor acquires a resource: // in this case, dynamic memory obtained via new[] person(const char* the_name, int the_age) { name = new char[strlen(the_name) + 1]; strcpy(name, the_name); age = the_age; } // the destructor must release this resource via delete[] ~person() { delete[] name; } };nameab

После выполнения тела деструктора и уничтожения любых автоматических объектов, выделенных в теле, деструктор для класса X вызывает деструкторы для прямых [...] членов X [n3126.pdf 12.4 A§6]

Управление ресурсами

Итак, когда мы должны объявить эти специальные функции-члены явно? Когда наш класс управляет ресурсом , то есть, когда объект класса отвечает за этот ресурс. Обычно это означает, что ресурс получен в конструкторе (или передан в конструктор) и выпущен в деструкторе.

Вернемся назад к предварительному стандарту C ++. Такого не было b, и программисты были влюблены в указатели. a.nameКласс мог выглядеть следующим образом :

a

Даже сегодня люди все еще пишут классы в этом стиле и попадают в беду: « Я подтолкнул человека к вектору, и теперь я получаю сумасшедшие ошибки памяти! » Помните, что по умолчанию копирование объекта означает копирование его элементов, но копирование nameэлемента просто копирует указатель, а не массив символов, на который он указывает! Это имеет несколько неприятных эффектов:

  1. Изменения через aможно наблюдать через b.
  2. Один раз bуничтожен, это свисающий указатель.// 1. copy constructor person(const person& that) { name = new char[strlen(that.name) + 1]; strcpy(name, that.name); age = that.age; } // 2. copy assignment operator person& operator=(const person& that) { if (this != &that) { delete[] name; // This is a dangerous point in the flow of execution! // We have temporarily invalidated the class invariants, // and the next statement might throw an exception, // leaving the object in an invalid state :( name = new char[strlen(that.name) + 1]; strcpy(name, that.name); age = that.age; } return *this; }
  3. Если aуничтожено, удаление оборванного указателя дает неопределенное поведение .
  4. Поскольку назначение не учитывает то, что nameуказывалось перед назначением, рано или поздно вы получите утечки памяти повсюду.

Явные определения

Так как копирование по порядку не имеет желаемого эффекта, мы должны явно определить конструктор копирования и оператор присваивания копии для создания глубоких копий массива символов:

name

Обратите внимание на разницу между инициализацией и присваиванием: мы должны снести старое состояние до назначения nameдля предотвращения утечек памяти. Кроме того, мы должны защищать себя от самостоятельного присвоения формы x = x. Без этой проверки delete[] nameудалите массив, содержащий исходную строку, потому что, когда вы пишете x = x, оба this->nameи that.nameсодержат один и тот же указатель.

Безопасность исключений

К сожалению, это решение провалится, если выйдет new char[...]исключение из-за исчерпания памяти. Одно из возможных решений - ввести локальную переменную и изменить порядок операторов:

// 2. copy assignment operator
person& operator=(const person& that)
{
    char* local_name = new char[strlen(that.name) + 1];
    // If the above statement throws,
    // the object is still in the same state as before.
    // None of the following statements will throw an exception :)
    strcpy(local_name, that.name);
    delete[] name;
    name = local_name;
    age = that.age;
    return *this;
}

Это также обеспечивает самостоятельное назначение без явной проверки. Еще более надежное решение этой проблемы - это идиома « копирование и своп» , но я не буду вдаваться в подробности безопасности исключений. Я упомянул только исключения, чтобы сделать следующее: Написание классов, которые управляют ресурсами, сложно.

Некопируемые ресурсы

Некоторые ресурсы не могут или не должны копироваться, например, дескрипторы файлов или мьютексы. В этом случае просто объявите конструктор копирования и оператор присваивания копии, privateне указывая определения:

private:

    person(const person& that);
    person& operator=(const person& that);

Кроме того, вы можете наследовать boost::noncopyableили объявить их как удаленные (C ++ 0x):

person(const person& that) = delete;
person& operator=(const person& that) = delete;

Правило трех

Иногда вам нужно реализовать класс, управляющий ресурсом. (Никогда не управляйте несколькими ресурсами в одном классе, это приведет только к боли.) В этом случае помните правило три :

Если вам нужно явно объявить либо деструктор, либо конструктор копирования, либо оператор присваивания копии самостоятельно, вам, вероятно, нужно явно объявить все три из них.

(К сожалению, это «правило» не применяется стандартом C ++ или любым компилятором, о котором я знаю.)

Совет

В большинстве случаев вам не нужно самостоятельно управлять ресурсом, потому что существующий класс, например, std::stringуже делает это за вас. Просто сравните простой код с помощью std::stringчлена с запутанной и подверженной ошибкам альтернативой, используя a, char*и вы должны быть уверены. Пока вы держитесь подальше от сырых указателей, правило три вряд ли касается вашего собственного кода.


455 ов

Правило трех является правилом для C ++, в основном говорят ,

Если ваш класс нуждается в

  • конструктор копирования ,
  • оператор присваивания ,
  • или деструктор ,

определенно эксплицитно, то, вероятно, потребуется все три из них .

Причины этого в том, что все три из них обычно используются для управления ресурсом, и если ваш класс управляет ресурсом, ему обычно необходимо управлять копированием, а также освобождать.

Если нет хорошей семантики для копирования ресурса, который управляет ваш класс, тогда учтите, что запретить копирование, объявив (не определяя ) конструктор копирования и оператор присваивания как private.

(Обратите внимание, что новая версия стандарта C ++ (которая является C ++ 11) добавляет семантику перемещения в C ++, что, скорее всего, изменит правило из трех. Однако я слишком мало знаю об этом, чтобы написать раздел C ++ 11 о правиле трех.)


136 ов

Закон большой тройки, как указано выше.

Легкий пример, на простом английском языке, той проблемы, которую он решает:

Нестандартный деструктор

Вы выделили память в своем конструкторе, и вам нужно написать деструктор, чтобы удалить его. В противном случае произойдет утечка памяти.

Вы можете подумать, что это работа.

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

Однажды один из них удалит память в своем деструкторе, другой будет иметь указатель на недопустимую память (это называется обвисший указатель), когда он пытается использовать его, все будет выглядеть волосатым.

Поэтому вы пишете конструктор копирования, чтобы он выделял новые объекты для уничтожения их собственных фрагментов памяти.

Оператор присваивания и конструктор копирования

Вы выделили память в своем конструкторе указателю на член вашего класса. Когда вы копируете объект этого класса, оператор присваивания по умолчанию и конструктор копирования копируют значение этого элемента-указателя на новый объект.

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

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


39

В принципе, если у вас есть деструктор (а не деструктор по умолчанию), это означает, что класс, который вы определили, имеет некоторое распределение памяти. Предположим, что класс используется снаружи некоторым кодом клиента или вами.

    MyClass x(a, b);
    MyClass y(c, d);
    x = y; // This is a shallow copy if assignment operator is not provided

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


30

Что означает копирование объекта? Есть несколько способов копирования объектов - давайте поговорим о 2 типах, которые вы, скорее всего, ссылаетесь на: глубокую копию и мелкую копию.

Поскольку мы находимся на объектно-ориентированном языке (или, по крайней мере, предполагаем это), предположим, что у вас есть выделенная часть памяти. Поскольку это OO-язык, мы можем легко ссылаться на куски памяти, которые мы выделяем, потому что они обычно являются примитивными переменными (ints, chars, bytes) или классами, которые мы определили, которые сделаны из наших собственных типов и примитивов. Итак, допустим, у нас есть класс автомобилей следующим образом:

class Car //A very simple class just to demonstrate what these definitions mean.
//It's pseudocode C++/Javaish, I assume strings do not need to be allocated.
{
private String sPrintColor;
private String sModel;
private String sMake;

public changePaint(String newColor)
{
   this.sPrintColor = newColor;
}

public Car(String model, String make, String color) //Constructor
{
   this.sPrintColor = color;
   this.sModel = model;
   this.sMake = make;
}

public ~Car() //Destructor
{
//Because we did not create any custom types, we aren't adding more code.
//Anytime your object goes out of scope / program collects garbage / etc. this guy gets called + all other related destructors.
//Since we did not use anything but strings, we have nothing additional to handle.
//The assumption is being made that the 3 strings will be handled by string's destructor and that it is being called automatically--if this were not the case you would need to do it here.
}

public Car(const Car &other) // Copy Constructor
{
   this.sPrintColor = other.sPrintColor;
   this.sModel = other.sModel;
   this.sMake = other.sMake;
}
public Car &operator =(const Car &other) // Assignment Operator
{
   if(this != &other)
   {
      this.sPrintColor = other.sPrintColor;
      this.sModel = other.sModel;
      this.sMake = other.sMake;
   }
   return *this;
}

}

Глубокая копия, если мы объявляем объект, а затем создаем полностью отдельную копию объекта ... мы заканчиваем двумя объектами в 2 полностью наборах памяти.

Car car1 = new Car("mustang", "ford", "red");
Car car2 = car1; //Call the copy constructor
car2.changePaint("green");
//car2 is now green but car1 is still red.

Теперь давайте сделаем что-то странное. Предположим, что // Неверный пример копирования // Предположим, что мы находимся на C ++, потому что это стандартное поведение - это объекты с мелкой копией, если у вас нет конструктора, написанного для операции. // Теперь давайте предположим, что у меня нет кода для операций присваивания или копирования, как я делаю выше ... с теми, кто сейчас ушел, C ++ будет использовать значение по умолчанию. Автомобиль car1 = новый автомобиль ( «ford» , «mustang» , «red» ); Автомобиль car2 = автомобиль1 ; автомобиль2 . changePaint ( «зеленый» ); // car1 также теперь зеленый delete car2 ; / * Я избавлюсь от своей машины, которая также является действительно вашей машиной ... Я сказал C ++ разрешить адрес, где car2 существует, и удалить память ... которая также является памятью, связанной с вашим автомобилем. * / Car1 . changePaint ( «красный» ); / *, вероятно, произойдет сбой, потому что эта область больше не выделяется программе. * / либо запрограммирована неправильно, либо намеренно предназначена для обмена фактической памятью, из которой сделан car1. (Обычно бывает ошибкой делать это, а в классах обычно это одеяло, о котором идет речь.) Представьте, что в любое время, когда вы спрашиваете о car2, вы действительно решаете указатель на пространство памяти car1 ... это более или менее то, что мелкая копия является.

Car car2 = car1;

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

Что такое конструктор копирования и оператор присваивания копии? Я уже использовал их выше. Конструктор копирования вызывается при вводе кода, например, по car2 = car1; существу, если вы объявляете переменную и назначаете ее в одной строке, то есть когда вы вызываете конструктор копирования. Оператор присваивания - это то, что происходит, когда вы используете знак равенства car2. Уведомление car2не объявляется в том же заявлении. Два куска кода, который вы пишете для этих операций, скорее всего, очень похожи. На самом деле в типичном шаблоне проектирования есть еще одна функция, которую вы вызываете, чтобы установить все, как только вы удовлетворены первоначальной копией / присваиванием, является законным - если вы посмотрите на код, который я написал, функции почти идентичны.

Когда мне нужно объявить их самостоятельно? Если вы не пишете код, который должен быть общим или для производства каким-либо образом, вам действительно нужно только объявить их, когда они вам понадобятся. Вам нужно знать, что делает ваш язык программирования, если вы решили использовать его «случайно» и не сделали этого - то есть вы получите компилятор по умолчанию. Например, я редко использую конструкторы копирования, но переопределения операторов присваивания очень распространены. Знаете ли вы, что можете переопределить, что такое сложение, вычитание и т. Д.?

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

C ++, копировать-конструктор, назначение-оператор, C ++ - чаво, правило трое,
Похожие вопросы