Питання Що таке правило трьох?


  • Що робить копіювання об'єкта означає?
  • Що таке? скопіювати конструктор і оператор присвоєння копії?
  • Коли мені потрібно заявити про себе?
  • Як я можу запобігти копіювання моїх об'єктів?

1844
2017-11-13 13:27


походження


Будь ласка читати ціла нитка і в c++-faq тег вікі перед тим, як голосувати, щоб закрити. - sbi
@Binary: принаймні, займіть час, щоб прочитати коментар дискусії раніше ви голосували. Текст було набагато простішим, але Фреду було запропоновано розгорнути його. Також, хоча це чотири питання граматично, це дійсно лише одне питання з кількома аспектами до нього. (Якщо ви не погоджуєтесь з цим, то доведіть свій POV, відповідаючи на кожен з цих питань самостійно і дайте нам проголосувати за результати.) - sbi
Фред, ось цікаве доповнення до вашої відповіді щодо C ++ 1x: stackoverflow.com/questions/4782757/.... Як ми маємо справу з цим? - sbi
Пов'язані: Закон Великої Двоє - Nemanja Trifunovic
Майте на увазі, що, починаючи з C ++ 11, я думаю, що це було оновлено до п'яти правил або щось подібне. - paxdiablo


Відповіді:


Вступ

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) частина це називається а список учасників ініціалізатора.)

Особливі функції члена

Що означає копіювати a person об'єкт The 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, і програмісти були закохані у покажчики. The 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;
}

Це також забезпечує самозабезпечення без чіткої перевірки. Ще більш надійним рішенням цієї проблеми є копіювання та перестановка ідіома, але я не буду вдаватися в деталі винятку безпеки тут. Я лише згадав про винятки, щоб зробити наступний пункт: Написання класів, що управляють ресурсами, важко.

Некопіювані ресурси

Деякі ресурси не можна або не повинні бути скопійовані, наприклад, файлові ручки чи mutex. У цьому випадку просто оголосити конструктор копії та оператор присвоювання копії як 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* і ви повинні бути переконані. До тих пір, поки ви не тримаєтеся в стороні від користувачів симетричних вказівок, правило трьох навряд чи стосується вашого власного коду.


1517
2017-11-13 13:27



Фред, я б відчував себе краще про моє проголосування, якщо (А) ви не напишете погано виконане завдання в копіюваному коді та додасте нотатку, яка говорить, що це неправильно, і шукати в іншому місці в дрібному роздруківці; або використовувати c & s у коді, або просто пропустити виконання всіх цих членів (B), ви скоротили першу половину, що мало пов'язано з RoT; (С) ви обговорите введення семантики переміщення і що це означає для RoT. - sbi
Але тоді пост повинен бути зроблений C / W, я думаю. Мені подобається, що ви дотримуєтеся в основному точних термінів (наприклад, ви кажете "скопіювати оператор присвоювання ", і ви не натискаєте на загальну пастку, що призначення не може означати копію). - Johannes Schaub - litb
@Prasoon: Я не думаю, що вирізання половини відповіді буде розглядатися як "справедливе редагування" відповіді, що не відповідає критеріям. - sbi
Також, можливо, я пройшов його повне редагування, але ви не згадуєте, що оператор повинен перевірити ідентифікацію, перш ніж робити що-небудь. - Björn Pollex
Було б чудово, якщо ви оновите свій пост для C ++ 11 (наприклад, перемістіть конструктор / призначення) - Alexander Malakhov


The Правило трьох це правило для C ++, в основному кажучи

Якщо ваш клас потребує будь-якого

  • a скопіювати конструктор,
  • ан оператор присвоєння,
  • або a деструктор,

чітко визначений, то це може знадобитися всі три з них.

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

Якщо для копіювання ресурсу, який управляє вашим класом, немає правильної семантики, то розгляньте заборону копіювання, декларуючи (не визначаючи) конструктор копії та оператор призначення як private.

(Зверніть увагу, що майбутня нова версія стандарту C ++ (тобто C ++ 11) додає переміщення семантики на C ++, що, ймовірно, змінить правило трьох. Проте я знаю надто мало про це, щоб написати розділ C ++ 11 про правило трьох.)


451
2017-11-13 14:22



Іншим рішенням для запобігання копіюванню є успадкування (приватно) з класу, який неможливо скопіювати (наприклад, boost::noncopyable) Це також може бути набагато чіткішим. Я думаю, що C ++ 0x і можливість "видалити" функції можуть допомогти тут, але забули синтаксис: / - Matthieu M.
@ Маттьє: Так, це теж працює. Але хіба що noncopyable є частиною std lib, я не вважаю це значним поліпшенням. (О, і якщо ви забули синтаксис видалення, ви забули mor ethan я колись знав. :)) - sbi
Будь-які оновлення за правилом трьох і C ++ 1x? - Daan Timmer
@ Дан: Див ця відповідь. Проте я б рекомендував дотримуватися Мартіньос Правило нуля. Для мене це одне з найважливіших правил для C ++, створеного в останнє десятиліття. - sbi
В даний час розташовано Правило нуля Мартіно тут - Diego


Закон великих трьох є таким, як зазначено вище.

Легкий приклад, на простому англомовному, про якусь проблему вирішує:

Деструктор за замовчуванням

Ви виділили пам'ять у вашому конструкторі, і тому вам потрібно написати деструктор, щоб видалити його. В іншому випадку ви викличете витік пам'яті.

Ви можете подумати, що це робота.

Проблема полягає в тому, що якщо копія виконана з вашого об'єкта, то копія вказує на ту саму пам'ять, що й оригінальний об'єкт.

Одного разу один з них видаляє пам'ять у своєму деструкторі, інший буде мати вказівник на недійсну пам'ять (це називається болісним покажчиком), коли він намагається використати його, воно збирається волохати.

Тому ви пишете конструктор копії, щоб він виділив нові об'єкти, щоб їх знищити.

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

Ви виділили пам'ять у вашому конструкторі для вказівника вашого класу. Під час копіювання об'єкта цього класу оператор призначення та копіювання за замовчуванням скопіюють значення цього покажчика члена на новий об'єкт.

Це означає, що новий об'єкт і старий об'єкт будуть вказувати на той самий фрагмент пам'яті, тому, коли ви зміните його в одному об'єкті, він буде змінено для іншого objerct теж. Якщо один об'єкт видаляє цю пам'ять, інший буде продовжувати намагатися його використовувати - eek.

Щоб вирішити це, ви пишете власну версію конструктора копії та оператора призначення. Ваші версії виділяють окрему пам'ять новим об'єктам і копіюють по значенням, які перший покажчик вказує, а не на адресу.


134
2018-05-14 14:22



Отже, якщо ми використовуємо конструктор копій, тоді копія виконана, але в іншому місці пам'яті взагалі, і якщо ми не використовуємо конструктор копій, то копіювання виконується, але воно вказує на ту ж саму область пам'яті. це те, що ви намагаєтеся сказати? Отже, копія без конструктора копії означає, що там буде новий покажчик, але вказує на ту ж саму пам'ять, однак, якщо конструктор копії явно визначений користувачем, то ми матимемо окремий покажчик, що вказує на іншу область пам'яті, але має дані. - Unbreakable
Вибачте, я відповів на ці віки тому, але моя відповідь, схоже, все ще не перебуває тут :-( У принципі, так, ви отримуєте це :-) - Stefan
Як принцип стосується оператора присвоєння копії? Ця відповідь була б більш корисною, якщо б було згадано 3-е місце у трьох правилах. - DBedrenko
@DBredrenko, "ви пишете конструктор копії так, щоб він виділяв нові об'єкти своїми частинами пам'яті ...", це той самий принцип, який поширюється на оператора присвоєння копії. Ви не думаєте, що я зробив це ясно? - Stefan
@ Стефан Так, спасибі! - DBedrenko


В принципі, якщо у вас є деструктор (а не деструктор за замовчуванням), це означає, що визначений вами клас має певний обсяг пам'яті. Припустимо, що клас використовується за межами деякого клієнтського коду або вами.

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

Якщо у MyClass є лише деякі примітивні типізовані учасники, то оператор призначення за замовчуванням буде працювати, але якщо він має певні елементи вказівників та об'єкти, у яких немає операторів присвоєння, результат буде непередбачуваним. Тому ми можемо сказати, що якщо в деструкторі класу є щось видалити, нам може знадобитися оператор глибокого копіювання, що означає, що ми повинні надавати конструктор копій і оператор призначення.


37
2017-12-31 19:29





Що означає копіювання об'єкта? Є кілька способів копіювання об'єктів - давайте поговоримо про два типи, які ви, швидше за все, маєте на увазі, - глибока копія та неглибока копія.

Оскільки ми перебуваємо в об'єктно-орієнтованій мові (або, принаймні, припускаємо це), скажімо, ви виділяєте частину пам'яті. Оскільки це 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.

Тепер давайте зробимо щось дивне. Скажімо, автомобіль 2 або запрограмований неправильно, або навмисно призначений для того, щоб поділитися фактичною пам'яттю, з якої складається 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 не заявлено в тій же заяві. Два шматочки коду, який ви пишете для цих операцій, ймовірно, дуже схожі. Насправді типовий шаблон дизайну має іншу функцію, яку ви телефонуєте, щоб встановити все, як тільки ви задовольняєтесь, первинна копія / призначення є законною. Якщо ви подивитеся на написаний довгий час, то ці функції майже ідентичні.

Коли мені потрібно заявити про себе? Якщо ви не пишете код, який буде використовуватися для спільного використання або якийсь вид продукції, вам потрібно лише заявити їх, коли вони вам потрібні. Ви повинні знати, що робить ваша мова програми, якщо ви вирішите використовувати її "випадково" і не зробили її, тобто. ви отримуєте за замовчуванням компілятор. Наприклад, я рідко використовую конструктори копіювання, але зауваження оператора призначення є дуже поширеними. Чи знали ви, що можете переоцінити те, що означає додавання, віднімання тощо?

Як я можу запобігти копіювання моїх об'єктів? Перевизначення всіх способів, яким ви дозволите розподілити пам'ять для вашого об'єкта за допомогою приватної функції, є розумним початком. Якщо ви дійсно не бажаєте, щоб люди копіювали їх, ви могли б зробити його загальнодоступним та сповістити програміста, кидаючи виняток, а також не копіюючи об'єкт.


27
2017-10-17 16:37



Питання позначено як C ++. Ця експозиція псевдокоду мало що дозволяє уточнити що-небудь про чітко визначену "Правило трьох" у кращому випадку і просто поширювати плутанину в гіршому випадку. - sehe


Коли мені потрібно заявити про себе?

Правило трьох говорить про те, що, якщо ви оголосите будь-яку а

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

19
2018-01-12 09:54





Багато існуючих відповідей вже торкається конструктора копії, оператора призначення та деструктора. Проте в пост C ++ 11 введення семантичного переміщення може розширити це за межами 3.

Нещодавно Майкл Клайс виступив з доповіддю, що стосується цієї теми: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class


9
2018-01-07 05:38





Правило трьох в C ++ є основним принципом розробки та розробки трьох вимог, які, якщо в одній з наступних функцій учасника є чітке визначення, то програміст повинен визначити спільні інші два функції. А саме три основні функції необхідні: деструктор, конструктор копій, оператор присвоєння копії.

Конструктор копій в C ++ - це спеціальний конструктор. Він використовується для створення нового об'єкта, який є новим об'єктом, еквівалентним копії існуючого об'єкта.

Оператор присвоювання копіювання - це спеціальний оператор призначення, який зазвичай використовується для вказування існуючого об'єкта іншим особам одного й того ж об'єкта.

Є приклади:

// default constructor
My_Class a;

// copy constructor
My_Class b(a);

// copy constructor
My_Class c = a;

// copy assignment operator
b = a;

5
2017-08-12 04:27



Привіт, ваша відповідь не містить нічого нового. Інші висвітлюють цей предмет на більшій глибині, а точніше - ваш відповідь приблизно і фактично неправильний у деяких місцях (а саме тут немає "обов'язково", це "дуже треба"). Це дійсно не варто вам, публікуючи подібну відповідь на питання, про які вже давно вже дано відповідь. Якщо у вас немає нових речей для додавання. - Mat
Також є чотири швидкі приклади, які є як-то пов'язані з два з три про те, що говорить про "Правило трьох". Занадто багато плутанини. - anatolyg