Что такое правило трех? - программирование

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

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

Ответ 1

Введение

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

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

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]

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

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

Вернемся назад к предварительному стандарту С++. Не было такой вещи, как 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 просто копирует указатель, а не массив символов, на который он указывает! Это имеет несколько неприятных эффектов:

  • Изменения через a можно наблюдать через b.
  • Как только b уничтожается, a.name является обвисшим указателем.
  • Если a уничтожен, удаление указателя оборванности дает undefined поведение.
  • Так как в присваивании не учитывается то, что указывал 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 или объявить их как удаленные (С++ 0x):

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

Правило трех

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

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

(К сожалению, это "правило" не применяется стандартом С++ или компилятором, о котором я знаю.)

Совет

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

Ответ 2

Правило трех - эмпирическое правило для С++, в основном говоря

Если ваш класс нуждается в любом из

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

определяется явно, тогда, вероятно, потребуется все три из них.

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

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

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

Ответ 3

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

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

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

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

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

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

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

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

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

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

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

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

Ответ 4

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

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

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

Ответ 5

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

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

class Car //A very simple class just to demonstrate what these definitions mean.
//It 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 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 standard behavior is to shallow copy objects if you do not have a constructor written for an operation.
//Now let 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 не объявляется в том же самом заявлении. Два куска кода, который вы пишете для этих операций, скорее всего, очень похожи. На самом деле в типичном шаблоне проектирования есть еще одна функция, которую вы вызываете, чтобы установить все, как только вы удовлетворены первоначальной копией/присваиванием, является законным - если вы посмотрите на код, который я написал, функции почти идентичны.

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

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

Ответ 6

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

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

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

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

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

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

Следствием правила трех является то, что наличие объявленного пользователем деструктора указывает на то, что простая копия члена не будет подходящей для операций копирования в классе. Это, в свою очередь, предполагает, что если класс объявляет деструктор, операции копирования, вероятно, не должны автоматически генерироваться, потому что они не будут делать правильные вещи. В то время, когда был принят С++ 98, значение этой линии рассуждений не было полностью оценено, поэтому в С++ 98 существование объявленного пользователем деструктора не повлияло на готовность компиляторов генерировать операции копирования. Это все еще имеет место в С++ 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);
}

В С++ 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);
}

Ответ 7

Многие из уже существующих ответов уже касаются конструктора копирования, оператора присваивания и деструктора. Однако в post С++ 11 введение семантики перемещения может расширить это значение выше 3.

Недавно Майкл Клайс рассказал, что касается этой темы: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class

Ответ 8

Правило из трех в С++ является основополагающим принципом проектирования и разработки трех требований, которые, если есть четкое определение в одной из следующих функций-членов, затем программист должен определить две другие функции-члены вместе. А именно, необходимы следующие три функции-члена: деструктор, конструктор копирования, оператор присваивания копии.

Конструктор копирования в С++ является специальным конструктором. Он используется для создания нового объекта, который является новым объектом, эквивалентным копии существующего объекта.

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

Быстрые примеры:

// default constructor
My_Class a;

// copy constructor
My_Class b(a);

// copy constructor
My_Class c = a;

// copy assignment operator
b = a;