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

2151
  • Что означает копирование объекта ?
  • Какой конструктор копирования и оператор копирующего присваивания ?
  • Когда я должен объявить их сам?
  • Как я могу предотвратить копирование моих объектов?
fredoverflow
источник
52
Пожалуйста , прочитайте всю эту тему и в c++-faqтеги вики , прежде чем голосовать , чтобы закрыть .
СБИ
13
@Binary: По крайней мере, найдите время, чтобы прочитать обсуждение комментариев, прежде чем голосовать. Раньше текст был намного проще, но Фреду было предложено расширить его. Кроме того, хотя это четыре вопроса грамматически , на самом деле это всего лишь один вопрос с несколькими аспектами. (Если вы не согласны с этим, то докажите свое POV, ответив на каждый из этих вопросов самостоятельно, и позвольте нам проголосовать за результаты.)
sbi
1
Фред, вот интересное дополнение к твоему ответу относительно C ++ 1x: stackoverflow.com/questions/4782757/… . Как мы справимся с этим?
ВОО
6
Связанный: Закон Большой Двое
Неманья Трифунович
4
Имейте в виду, что, начиная с C ++ 11, я думаю, что это было обновлено до правила пяти или что-то в этом роде.
paxdiablo

Ответы:

1795

Введение

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

fredoverflow
источник
4
Фред, я бы лучше отнесся к своему положительному голосованию, если бы (A) вы не указали плохо реализованное назначение в копируемом коде и добавили бы примечание о том, что это неправильно, и посмотрите в другом месте в мелкой печати; либо используйте в коде c & s, либо просто пропустите реализацию всех этих членов (B), вы бы сократили первую половину, которая не имеет ничего общего с RoT; (C) вы бы обсудили введение семантики перемещения и что это значит для RoT.
2010 г.
7
Но тогда пост должен быть сделан C / W, я думаю. Мне нравится, что вы сохраняете термины в основном точными (то есть, что вы говорите « оператор назначения копирования », и что вы не используете общую ловушку, что назначение не может подразумевать копию).
Йоханнес Шауб -
4
@Prasoon: я не думаю, что сокращение половины ответа будет рассматриваться как «честное редактирование» ответа не-CW.
СБИ
69
Было бы здорово, если вы обновите свой пост для C ++ 11 (т.е. переместите конструктор / назначение)
Александр Малахов
5
@solalito Все, что вы должны освободить после использования: блокировки параллелизма, дескрипторы файлов, соединения с базой данных, сетевые сокеты, куча памяти ...
fredoverflow
510

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

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

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

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

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

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

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

SBI
источник
3
Другим решением для предотвращения копирования является наследование (конфиденциально) от класса, который не может быть скопирован (например boost::noncopyable). Это также может быть намного понятнее. Я думаю, что C ++ 0x и возможность «удалять» функции могли бы помочь здесь, но забыл синтаксис: /
Matthieu M.
2
@Matthieu: Да, это тоже работает. Но если только он noncopyableне входит в стандартную библиотеку, я не считаю это большим улучшением. (О, и если вы забыли синтаксис удаления, вы забыли mor ethan, который я когда-либо знал. :))
sbi
3
@ Даан: см. Этот ответ . Тем не менее, я рекомендую придерживаться Martinho «s Правило Зеро . Для меня это одно из самых важных практических правил для C ++, созданных за последнее десятилетие.
СБИ
3
Правило нулевого Мартино теперь лучше (без явного захвата рекламного ПО), расположенное на archive.org
Натан Кидд,
161

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

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

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

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

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

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

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

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

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

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

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

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

Стефан
источник
4
Таким образом, если мы используем конструктор копирования, то копия создается, но в другом месте памяти, и если мы не используем конструктор копирования, то копирование выполняется, но оно указывает на то же место памяти. это то, что вы пытаетесь сказать? Таким образом, копия без конструктора копирования означает, что там будет новый указатель, но указывающий на ту же самую область памяти, однако, если у нас есть конструктор копирования, явно определенный пользователем, то у нас будет отдельный указатель, указывающий на другую область памяти, но имеющую данные.
Нерушимый
4
Извините, я отвечал на это давным-давно, но мой ответ, похоже, все еще не был здесь :-( В принципе, да - вы поняли :-)
Stefan
1
Как этот принцип распространяется на оператор присвоения копии? Этот ответ был бы более полезным, если бы упомянул третий в правиле трех.
Д.Бедренко
1
@DBedrenko, «вы пишете конструктор копирования, чтобы он выделял новые объекты своим собственным частям памяти ...» - это тот же принцип, который распространяется на оператор присваивания копии. Ты не думаешь, что я ясно дал понять?
Стефан
2
@DBedrenko, я добавил еще немного информации. Это делает это более ясным?
Стефан
44

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

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

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

fatma.ekici
источник
36

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

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

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

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

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

user1701047
источник
5
Вопрос был помечен C ++. Эта демонстрация псевдокода мало что проясняет в лучшем случае четко определенное «Правило трех», а в худшем - просто вносит путаницу.
Сехе
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);
}
Аджай Ядав
источник
16

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

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

вэй
источник
10

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

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

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

Есть быстрые примеры:

// default constructor
My_Class a;

// copy constructor
My_Class b(a);

// copy constructor
My_Class c = a;

// copy assignment operator
b = a;
Маркус Торнтон
источник
7
Привет, твой ответ не добавляет ничего нового. Другие охватывают эту тему гораздо глубже и точнее - ваш ответ является приблизительным и в некоторых местах фактически неправильным (а именно, здесь нет «необходимо»; это «очень вероятно, следует»). Не стоит тратить время на публикацию такого ответа на вопросы, на которые уже дан полный ответ. Если у вас нет новых вещей, чтобы добавить.
Мат
1
Кроме того, есть четыре быстрых примера, которые так или иначе связаны с двумя из трех , о которых говорится в правиле трех. Слишком много путаницы.
Анатолий