¿Cuál es la regla de tres?

2151
  • ¿Qué significa copiar un objeto ?
  • ¿Cuáles son el constructor de copia y el operador de asignación de copia ?
  • ¿Cuándo debo declararlos yo mismo?
  • ¿Cómo puedo evitar que se copien mis objetos?
flujo libre
fuente
52
Por favor lea todo este hilo y la c++-faqetiqueta wiki antes de votar para cerrar .
sbi
13
@Binary: al menos tómese el tiempo para leer la discusión de comentarios antes de emitir un voto. El texto solía ser mucho más simple, pero se le pidió a Fred que lo ampliara. Además, si bien son cuatro preguntas gramaticalmente , en realidad es solo una pregunta con varios aspectos. (Si no está de acuerdo con eso, pruebe su punto de vista respondiendo cada una de esas preguntas por su cuenta y déjenos votar sobre los resultados.)
sbi
1
Fred, aquí hay una adición interesante a tu respuesta con respecto a C ++ 1x: stackoverflow.com/questions/4782757/… . ¿Cómo nos enfrentamos a esto?
sbi
66
Relacionado: La ley de los dos grandes
Nemanja Trifunovic
44
Tenga en cuenta que, a partir de C ++ 11, creo que esto se ha actualizado a la regla de cinco, o algo así.
paxdiablo

Respuestas:

1795

Introducción

C ++ trata las variables de tipos definidos por el usuario con semántica de valores . Esto significa que los objetos se copian implícitamente en varios contextos, y debemos entender lo que realmente significa "copiar un objeto".

Permítanos considerar un ejemplo sencillo:

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?
}

(Si está desconcertado por la name(name), age(age)parte, esto se llama una lista de inicializador de miembros ).

Funciones especiales para miembros

¿Qué significa copiar un personobjeto? La mainfunción muestra dos escenarios de copia distintos. La inicialización person b(a);es realizada por el constructor de la copia . Su trabajo es construir un objeto nuevo basado en el estado de un objeto existente. La asignación la b = arealiza el operador de asignación de copia . Su trabajo es generalmente un poco más complicado, porque el objeto de destino ya está en algún estado válido que necesita ser tratado.

Como no declaramos a nosotros ni el constructor de la copia ni el operador de asignación (ni el destructor), estos se definen implícitamente para nosotros. Cita del estándar:

El constructor de [...] copia y el operador de asignación de copia, [...] y el destructor son funciones miembro especiales. [ Nota : La implementación declarará implícitamente estas funciones miembro para algunos tipos de clase cuando el programa no las declare explícitamente. La implementación los definirá implícitamente si se usan. [...] nota final ] [n3126.pdf sección 12 §1]

Por defecto, copiar un objeto significa copiar sus miembros:

El constructor de copia implícitamente definido para una clase X sin unión realiza una copia miembro de sus subobjetos. [n3126.pdf sección 12.8 §16]

El operador de asignación de copia definido implícitamente para una clase X no sindicalizada realiza la asignación de copia miembro de sus subobjetos. [n3126.pdf sección 12.8 §30]

Definiciones implícitas

Las funciones de miembro especiales definidas implícitamente para persontener este aspecto:

// 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()
{
}

La copia a nivel de miembro es exactamente lo que queremos en este caso: namey agese copian, por lo que obtenemos un objeto autónomo e independiente person. El destructor definido implícitamente siempre está vacío. Esto también está bien en este caso ya que no adquirimos ningún recurso en el constructor. Los destructores de los miembros se invocan implícitamente después de que el persondestructor finaliza:

Después de ejecutar el cuerpo del destructor y destruir los objetos automáticos asignados dentro del cuerpo, un destructor para la clase X llama a los destructores para los miembros directos de X [n3126.pdf 12.4 §6]

Administrar recursos

Entonces, ¿cuándo debemos declarar explícitamente esas funciones miembro especiales? Cuando nuestra clase gestiona un recurso , es decir, cuando un objeto de la clase es responsable de ese recurso. Eso generalmente significa que el recurso se adquiere en el constructor (o se pasa al constructor) y se libera en el destructor.

Regresemos en el tiempo a C ++ preestándar. No existía tal cosa std::string, y los programadores estaban enamorados de los punteros. La personclase podría haberse visto así:

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

Incluso hoy, la gente todavía escribe clases en este estilo y se mete en problemas: " ¡Empujé a una persona a un vector y ahora tengo errores de memoria locos! " Recuerde que, de forma predeterminada, copiar un objeto significa copiar a sus miembros, pero copiar al namemiembro simplemente copia un puntero, no el conjunto de caracteres al que apunta. Esto tiene varios efectos desagradables:

  1. Los cambios a través de ase pueden observar a través de b.
  2. Una vez que bse destruye, a.namees un puntero que cuelga.
  3. Si ase destruye, eliminar el puntero colgante produce un comportamiento indefinido .
  4. Dado que la asignación no tiene en cuenta lo que nameseñalaba antes de la asignación, tarde o temprano obtendrá pérdidas de memoria por todas partes.

Definiciones explícitas

Como la copia a nivel de miembro no tiene el efecto deseado, debemos definir el constructor de copia y el operador de asignación de copia explícitamente para hacer copias profundas de la matriz de caracteres:

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

Tenga en cuenta la diferencia entre inicialización y asignación: debemos derribar el estado anterior antes de asignar a namepara evitar pérdidas de memoria. Además, tenemos que protegernos contra la autoasignación del formulario x = x. Sin esa verificación, delete[] namesería eliminar la matriz que contiene la fuente de la cadena, porque cuando se escribe x = x, tanto this->namey that.namecontener el mismo puntero.

Seguridad de excepción

Desafortunadamente, esta solución fallará si new char[...]arroja una excepción debido al agotamiento de la memoria. Una posible solución es introducir una variable local y reordenar las declaraciones:

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

Esto también se encarga de la autoasignación sin una verificación explícita. Una solución aún más robusta para este problema es el modismo de copiar e intercambiar , pero no entraré aquí en los detalles de seguridad de excepción. Solo mencioné excepciones para hacer el siguiente punto: Escribir clases que administren recursos es difícil.

Recursos no cobrables

Algunos recursos no pueden o no deben copiarse, como los identificadores de archivos o mutexes. En ese caso, simplemente declare el constructor de copia y el operador de asignación de copia como privatesin dar una definición:

private:

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

Alternativamente, puede heredarlos boost::noncopyableo declararlos como eliminados (en C ++ 11 y superior):

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

La regla de tres

A veces necesita implementar una clase que gestione un recurso. (Nunca administre múltiples recursos en una sola clase, esto solo provocará dolor). En ese caso, recuerde la regla de tres :

Si necesita declarar explícitamente el destructor, el constructor de copia o el operador de asignación de copia usted mismo, probablemente deba declarar explícitamente los tres.

(Desafortunadamente, esta "regla" no es aplicada por el estándar C ++ ni por ningún compilador que conozca).

La regla de cinco

A partir de C ++ 11, un objeto tiene 2 funciones miembro especiales adicionales: el constructor de movimiento y la asignación de movimiento. La regla de cinco estados para implementar estas funciones también.

Un ejemplo con las firmas:

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

La regla de cero

La regla de 3/5 también se conoce como la regla de 0/3/5. La parte cero de la regla establece que se le permite no escribir ninguna de las funciones especiales del miembro al crear su clase.

Consejo

La mayoría de las veces, no necesita administrar un recurso usted mismo, porque una clase existente como std::stringya lo hace por usted. Simplemente compare el código simple usando un std::stringmiembro con la alternativa enrevesada y propensa a errores usando a char*y debería estar convencido. Siempre y cuando se mantenga alejado de los miembros de puntero sin procesar, es poco probable que la regla de tres se refiera a su propio código.

flujo libre
fuente
44
Fred, me sentiría mejor acerca de mi voto positivo si (A) no deletrearas la asignación mal implementada en el código copiable y agregues una nota que diga que está mal y que busques en otra parte de la letra pequeña; use c & s en el código o simplemente omita la implementación de todos estos miembros (B), acortaría la primera mitad, que tiene poco que ver con el RoT; (C) discutiría la introducción de la semántica de movimiento y lo que eso significa para el RoT.
sbi
77
Pero entonces la publicación debería hacerse C / W, creo. Me gusta que mantenga los términos en su mayoría precisos (es decir, que diga " operador de asignación de copia ", y que no aproveche la trampa común de que la asignación no podría implicar una copia).
Johannes Schaub - litb
44
@Prasoon: No creo que cortar la mitad de la respuesta se vea como una "edición justa" de una respuesta que no sea CW.
sbi
69
Sería genial si actualiza su publicación para C ++ 11 (es decir, mover constructor / asignación)
Alexander Malakhov
55
Cualquier cosa @solalito hay que liberar después de su uso: las cerraduras de concurrencia, manipuladores de archivos, conexiones de bases de datos, conexiones de red, la pila de memoria ...
fredoverflow
510

La regla de tres es una regla general para C ++, básicamente diciendo

Si tu clase necesita alguno de

  • un constructor de copias ,
  • un operador de asignación ,
  • o un destructor ,

definido explícitamente, entonces es probable que los necesite a los tres .

La razón de esto es que los tres generalmente se usan para administrar un recurso, y si su clase administra un recurso, generalmente necesita administrar la copia y la liberación.

Si no hay una buena semántica para copiar el recurso que administra su clase, considere prohibir la copia declarando (no definiendo ) el constructor de copia y el operador de asignación como private.

(Tenga en cuenta que la próxima nueva versión del estándar C ++ (que es C ++ 11) agrega semántica de movimiento a C ++, lo que probablemente cambiará la Regla de Tres. Sin embargo, sé muy poco sobre esto para escribir una sección C ++ 11 sobre la regla de tres.)

sbi
fuente
3
Otra solución para evitar la copia es heredar (en privado) de una clase que no se puede copiar (como boost::noncopyable). También puede ser mucho más claro. Creo que C ++ 0x y la posibilidad de "eliminar" funciones podrían ayudar aquí, pero olvidé la sintaxis: /
Matthieu M.
2
@ Matthieu: Sí, eso también funciona. Pero a menos que noncopyablesea ​​parte de la biblioteca estándar, no lo considero una gran mejora. (Ah, y si se le olvidó la sintaxis eliminación, se le olvidó que mor Ethan he conocido. :))
SBI
3
@Daan: Mira esta respuesta . Sin embargo, recomendaría apegarse a la Regla de cero de Martinho . Para mí, esta es una de las reglas generales más importantes para C ++ acuñado en la última década.
sbi
3
La regla de cero de Martinho ahora mejor (sin aparente adquisición de adware) ubicada en archive.org
Nathan Kidd
161

La ley de los tres grandes es como se especifica arriba.

Un ejemplo sencillo, en inglés simple, del tipo de problema que resuelve:

Destructor no predeterminado

Usted asignó memoria en su constructor y, por lo tanto, debe escribir un destructor para eliminarlo. De lo contrario, provocará una pérdida de memoria.

Se podría pensar que este es un trabajo hecho.

El problema será que, si se hace una copia de su objeto, la copia apuntará a la misma memoria que el objeto original.

Una vez, uno de estos elimina la memoria en su destructor, el otro tendrá un puntero a memoria no válida (esto se llama puntero colgante) cuando intenta usarlo, las cosas se pondrán difíciles.

Por lo tanto, usted escribe un constructor de copia para que asigne a los nuevos objetos sus propios pedazos de memoria para destruir.

Operador de asignación y constructor de copia

Asignó memoria en su constructor a un puntero miembro de su clase. Cuando copie un objeto de esta clase, el operador de asignación predeterminado y el constructor de copia copiarán el valor de este puntero miembro al nuevo objeto.

Esto significa que el nuevo objeto y el antiguo objeto apuntarán al mismo trozo de memoria, por lo que cuando lo cambie en un objeto, también se cambiará por el otro objeto. Si un objeto elimina esta memoria, el otro continuará tratando de usarlo - eek.

Para resolver esto, escriba su propia versión del constructor de copia y el operador de asignación. Sus versiones asignan memoria separada a los nuevos objetos y copian los valores a los que apunta el primer puntero en lugar de su dirección.

Stefan
fuente
44
Entonces, si usamos un constructor de copias, la copia se realiza pero en una ubicación de memoria completamente diferente y si no usamos el constructor de copias, la copia se realiza pero apunta a la misma ubicación de memoria. ¿Es eso lo que estás tratando de decir? Por lo tanto, una copia sin constructor de copia significa que habrá un nuevo puntero pero que apuntará a la misma ubicación de memoria; sin embargo, si tenemos un constructor de copia definido explícitamente por el usuario, tendremos un puntero separado que apunte a una ubicación de memoria diferente pero con los datos.
Irrompible el
44
Lo siento, respondí esto hace mucho tiempo, pero parece que mi respuesta todavía no está aquí :-( Básicamente, sí, lo entiendes :-)
Stefan
1
¿Cómo se extiende el principio al operador de asignación de copia? Esta respuesta sería más útil si se mencionara el tercero en la Regla de Tres.
DBedrenko
1
@DBedrenko, "usted escribe un constructor de copia para que asigne a los nuevos objetos sus propias piezas de memoria ..." este es el mismo principio que se extiende al operador de asignación de copias. ¿No crees que lo he dejado claro?
Stefan
2
@DBedrenko, he agregado más información. Eso lo hace más claro?
Stefan
44

Básicamente, si tiene un destructor (no el destructor predeterminado) significa que la clase que definió tiene alguna asignación de memoria. Suponga que la clase es utilizada por algún código de cliente o por usted.

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

Si MyClass solo tiene algunos miembros tipados primitivos, un operador de asignación predeterminado funcionaría, pero si tiene algunos miembros de puntero y objetos que no tienen operadores de asignación, el resultado sería impredecible. Por lo tanto, podemos decir que si hay algo que eliminar en el destructor de una clase, podríamos necesitar un operador de copia profunda, lo que significa que deberíamos proporcionar un constructor de copia y un operador de asignación.

fatma.ekici
fuente
36

¿Qué significa copiar un objeto? Hay algunas formas de copiar objetos: hablemos de los 2 tipos a los que probablemente se refiere: copia profunda y copia superficial.

Dado que estamos en un lenguaje orientado a objetos (o al menos así lo suponemos), supongamos que tiene un pedazo de memoria asignado. Como es un lenguaje OO, podemos referirnos fácilmente a fragmentos de memoria que asignamos porque generalmente son variables primitivas (ints, chars, bytes) o clases que definimos que están hechas de nuestros propios tipos y primitivas. Entonces, digamos que tenemos una clase de Car de la siguiente manera:

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

}

Una copia profunda es si declaramos un objeto y luego creamos una copia completamente separada del objeto ... terminamos con 2 objetos en 2 conjuntos completos de memoria.

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.

Ahora hagamos algo extraño. Digamos que car2 está programado incorrectamente o intencionalmente para compartir la memoria real de la que está hecho car1. (Por lo general, es un error hacer esto y en las clases suele ser la cobija que se discute debajo). Suponga que cada vez que pregunta sobre car2, realmente está resolviendo un puntero al espacio de memoria de car1 ... eso es más o menos una copia superficial es.

//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.*/

Entonces, independientemente del idioma en el que esté escribiendo, tenga mucho cuidado con lo que quiere decir cuando se trata de copiar objetos porque la mayoría de las veces desea una copia profunda.

¿Cuáles son el constructor de copia y el operador de asignación de copia? Ya los he usado arriba. Se llama al constructor de copias cuando escribe código como Car car2 = car1; Esencialmente si declara una variable y la asigna en una línea, es entonces cuando se llama al constructor de copias. El operador de asignación es lo que sucede cuando usa un signo igual-- car2 = car1;. El aviso car2no se declara en la misma declaración. Es probable que los dos fragmentos de código que escriba para estas operaciones sean muy similares. De hecho, el patrón de diseño típico tiene otra función a la que llama para configurar todo una vez que está satisfecho, la copia / asignación inicial es legítima: si observa el código escrito a mano, las funciones son casi idénticas.

¿Cuándo debo declararlos yo mismo? Si no está escribiendo código que se va a compartir o para la producción de alguna manera, realmente solo necesita declararlos cuando los necesite. Debe saber qué hace el lenguaje de su programa si elige usarlo 'por accidente' y no creó uno, es decir, obtiene el compilador predeterminado. Raramente utilizo constructores de copia, por ejemplo, pero las anulaciones de operadores de asignación son muy comunes. ¿Sabía que también puede anular el significado de suma, resta, etc.?

¿Cómo puedo evitar que se copien mis objetos? Anular todas las formas en que se le permite asignar memoria para su objeto con una función privada es un comienzo razonable. Si realmente no desea que la gente los copie, puede hacerlo público y alertar al programador lanzando una excepción y tampoco copiando el objeto.

usuario1701047
fuente
55
La pregunta fue etiquetada como C ++. Esta exposición de pseudocódigo hace poco para aclarar algo sobre la bien definida "Regla de los tres" en el mejor de los casos, y solo difunde la confusión en el peor.
sehe
26

¿Cuándo debo declararlos yo mismo?

La Regla de los Tres establece que si declaras cualquiera de

  1. constructor de copia
  2. operador de asignación de copia
  3. incinerador de basuras

entonces deberías declarar los tres. Surgió de la observación de que la necesidad de asumir el significado de una operación de copia casi siempre se derivaba de que la clase realizaba algún tipo de gestión de recursos, y eso casi siempre implicaba que

  • cualquier gestión de recursos que se estaba haciendo en una operación de copia probablemente debía hacerse en la otra operación de copia y

  • el destructor de clase también estaría participando en la gestión del recurso (generalmente soltándolo). El recurso clásico que se administraba era la memoria, y esta es la razón por la cual todas las clases de la Biblioteca estándar que administran la memoria (por ejemplo, los contenedores STL que realizan la administración dinámica de la memoria) declaran "los tres grandes": operaciones de copia y un destructor.

Una consecuencia de la regla de tres es que la presencia de un destructor declarado por el usuario indica que es improbable que la copia simple del miembro sea apropiada para las operaciones de copia en la clase. Eso, a su vez, sugiere que si una clase declara un destructor, las operaciones de copia probablemente no deberían generarse automáticamente, porque no harían lo correcto. En el momento en que se adoptó C ++ 98, la importancia de esta línea de razonamiento no se apreciaba completamente, por lo que en C ++ 98, la existencia de un destructor declarado por el usuario no tuvo impacto en la disposición de los compiladores para generar operaciones de copia. Ese sigue siendo el caso en C ++ 11, pero solo porque restringir las condiciones bajo las cuales se generan las operaciones de copia rompería demasiado código heredado.

¿Cómo puedo evitar que se copien mis objetos?

Declare el constructor de copia y el operador de asignación de copia como especificador de acceso privado.

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

En C ++ 11 en adelante también puede declarar que el constructor de copia y el operador de asignación han sido eliminados

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

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


int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}
Ajay yadav
fuente
16

Muchas de las respuestas existentes ya tocan el constructor de copia, el operador de asignación y el destructor. Sin embargo, en post C ++ 11, la introducción del movimiento semántico puede expandir esto más allá de 3.

Recientemente, Michael Claisse dio una charla que toca este tema: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class

wei
fuente
10

La regla de tres en C ++ es un principio fundamental del diseño y el desarrollo de tres requisitos que si hay una definición clara en una de las siguientes funciones miembro, entonces el programador debe definir las otras funciones miembros juntas. Es decir, las siguientes tres funciones miembro son indispensables: destructor, constructor de copia, operador de asignación de copia.

Copiar constructor en C ++ es un constructor especial. Se utiliza para construir un nuevo objeto, que es el nuevo objeto equivalente a una copia de un objeto existente.

El operador de asignación de copia es un operador de asignación especial que generalmente se usa para especificar un objeto existente a otros del mismo tipo de objeto.

Hay ejemplos rápidos:

// default constructor
My_Class a;

// copy constructor
My_Class b(a);

// copy constructor
My_Class c = a;

// copy assignment operator
b = a;
Marcus Thornton
fuente
77
Hola, tu respuesta no agrega nada nuevo. Los otros cubren el tema con mucha más profundidad y con mayor precisión: su respuesta es aproximada y, de hecho, incorrecta en algunos lugares (es decir, aquí no hay un "must"; es "muy probablemente debería"). Realmente no valdría la pena publicar este tipo de respuestas a preguntas que ya han sido respondidas a fondo. A menos que tenga cosas nuevas que agregar.
Mat
1
Además, hay cuatro ejemplos rápidos, que de alguna manera están relacionados con dos de los tres de los que habla la Regla de los Tres. Demasiada confusión.
anatolyg