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


  • Что значит копирование объекта имею в виду?
  • Что такое копировать конструктор и оператор присваивания копии ?
  • Когда мне нужно объявить их самостоятельно?
  • Как я могу предотвратить копирование моих объектов?

1819


источник


Ответы:


Введение

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

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

class person
{
    std::string name;
    int age;

public:

    person(const std::string& name, int age) : name(name), age(age)
    {
    }
};

int main()
{
    person a("Bjarne Stroustrup", 60);
    person b(a);   // What happens here?
    b = a;         // And here?
}

(Если вас озадачивает name(name), age(age)часть, это называется список инициализаторов членов .)

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

Что значит копировать personобъект? mainфункция показывает два разных сценария копирования. Инициализация person b(a);осуществляется копировать конструктор , Его задача - построить новый объект, основанный на состоянии существующего объекта. Назначение b = aосуществляется оператор присваивания копии , Его работа, как правило, немного сложнее, потому что целевой объект уже находится в некотором правильном состоянии, с которым нужно иметь дело.

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

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

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

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

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

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

Неявно определенные специальные функции-члены для 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деструктор закончен:

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

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

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

Вернемся назад к предварительному стандарту C ++. Не было такого понятия, как 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;
    }
};

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

  1. Изменения через aможно наблюдать через b,
  2. однажды bразрушается, a.nameэто свисающий указатель.
  3. Если aуничтожается, удаляя указатель висячего указателя неопределенное поведение ,
  4. Поскольку в задании не учитывается то, что nameуказав перед назначением, рано или поздно вы получите утечки памяти повсюду.

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

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

// 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;
}

Обратите внимание на разницу между инициализацией и присваиванием: мы должны разорвать прежнее состояние, прежде чем 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член к запутанной и подверженной ошибкам альтернативе, используя char*и вы должны быть уверены. Пока вы держитесь подальше от сырых указателей, правило три вряд ли касается вашего собственного кода.


1500



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

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

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

определяется явно, то это, вероятно, понадобится все трое ,

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

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

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


445



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

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

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

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

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

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

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

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

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

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

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

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


132



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

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

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


36



Что означает копирование объекта? Есть несколько способов копирования объектов - давайте поговорим о 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.

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

//Shallow copy example
//Assume we're in C++ because it's standard behavior is to shallow copy objects if you do not have a constructor written for an operation.
//Now let's assume I do not have any code for the assignment or copy operations like I do above...with those now gone, C++ will use the default.

 Car car1 = new Car("ford", "mustang", "red"); 
 Car car2 = car1; 
 car2.changePaint("green");//car1 is also now green 
 delete car2;/*I get rid of my car which is also really your car...I told C++ to resolve 
 the address of where car2 exists and delete the memory...which is also
 the memory associated with your car.*/
 car1.changePaint("red");/*program will likely crash because this area is
 no longer allocated to the program.*/

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

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

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

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


26



Когда мне нужно объявить их самостоятельно?

Правило трех гласит, что если вы объявляете какой-либо из

  1. копировать конструктор
  2. оператор присваивания копии
  3. деструктор

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

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

  • деструктор класса также будет участвовать в управлении ресурсом (обычно его освобождая). Классическим ресурсом для управления была память, и именно поэтому все классы стандартной библиотеки, которые (например, контейнеры STL, которые выполняют управление динамической памятью), все объявляют «большой три»: обе операции копирования и деструктор.

Следствием правила трех заключается в том, что присутствие объявленного пользователем деструктора указывает на то, что простая копия элемента с несколькими членами вряд ли будет подходящей для операций копирования в классе. Это, в свою очередь, предполагает, что если класс объявляет деструктор, операции копирования, вероятно, не должны автоматически генерироваться, потому что они не будут поступать правильно. В то время, когда C ++ 98 был принят, значение этой линии рассуждений не было полностью оценено, поэтому в C ++ 98 существование объявленного пользователем деструктора не повлияло на готовность компиляторов генерировать операции копирования. Это все еще имеет место в C ++ 11, но только потому, что ограничение условий, при которых генерируются операции копирования, приведет к сломанию слишком большого количества устаревшего кода.

Как я могу предотвратить копирование моих объектов?

Объявить конструктор копирования и оператор присваивания копии как спецификатор частного доступа.

class MemoryBlock
{
public:

//code here

private:
MemoryBlock(const MemoryBlock& other)
{
   cout<<"copy constructor"<<endl;
}

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
 return *this;
}
};

int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}

В C ++ 11 вы также можете объявить, что оператор-конструктор и оператор присваивания удалены

class MemoryBlock
{
public:
MemoryBlock(const MemoryBlock& other) = delete

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
};


int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}

17



Many of the existing answers already touch the copy constructor, assignment operator and destructor. However, in post C++11, the introduction of move semantic may expand this beyond 3.

Recently Michael Claisse gave a talk that touches this topic: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class


8



Rule of three in C++ is a fundamental principle of the design and the development of three requirements that if there is clear definition in one of the following member function, then the programmer should define the other two members functions together. Namely the following three member functions are indispensable: destructor, copy constructor, copy assignment operator.

Copy constructor in C++ is a special constructor. It is used to build a new object, which is the new object equivalent to a copy of an existing object.

Copy assignment operator is a special assignment operator that is usually used to specify an existing object to others of the same type of object.

There are quick examples:

// default constructor
My_Class a;

// copy constructor
My_Class b(a);

// copy constructor
My_Class c = a;

// copy assignment operator
b = a;

4