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

неа. Он работает в функции, потому что содержимое функции выполняется во время вызова. Но содержимое класса выполняется в time-time, и в этот момент класс еще не существует.

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

class A(object):
    spam= 1

some_func(A)

A.eggs= 2

def _A_scramble(self):
    self.spam=self.eggs= 0
A.scramble= _A_scramble

Однако довольно необычно хотеть вызывать функцию в классе в середине своего собственного определения. Непонятно, что вы пытаетесь сделать, но, скорее всего, вам будет лучше decorators (или относительно новые декодеры класса ).

2027
задан Rann Lifshitz 26 May 2018 в 12:07
поделиться

4 ответа

Введение

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 удалит массив, содержащий строку source , потому что когда вы пишете 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 ++ 11 и выше):

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

Правило трех

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

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

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

Правило пяти

Из C ++ 11 на, объект имеет 2 дополнительные специальные функции-члены: конструктор перемещения и назначение перемещения. Правило пяти государств для реализации этих функций также.

Пример с подписями:

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

public:
    person(const std::string& name, int age);        // Ctor
    person(const person &) = default;                // Copy Ctor
    person(person &&) noexcept = default;            // Move Ctor
    person& operator=(const person &) = default;     // Copy Assignment
    person& operator=(person &&) noexcept = default; // Move Assignment
    ~person() noexcept = default;                    // Dtor
};

Правило нуля

Правило 3/5 также упоминается как правило 0/3/5. Нулевая часть правила гласит, что вы не можете писать какие-либо специальные функции-члены при создании вашего класса.

Совет

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

1672
ответ дан JVApen 26 May 2018 в 12:07
поделиться

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

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

14
ответ дан wei 26 May 2018 в 12:07
поделиться

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

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

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 объекта в 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 не объявлено в том же заявлении. Две части кода, которые вы пишете для этих операций, вероятно, очень похожи. На самом деле типичный шаблон проектирования имеет другую функцию, которую вы вызываете, чтобы установить все, как только вы убедитесь, что первоначальное копирование / присвоение является законным - если вы посмотрите на написанный мной от руки код, функции почти идентичны.

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

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

35
ответ дан David Szalai 26 May 2018 в 12:07
поделиться

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

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

Деструктор не по умолчанию

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

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

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

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

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

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

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

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

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

151
ответ дан rmobis 26 May 2018 в 12:07
поделиться
Другие вопросы по тегам:

Похожие вопросы: