Pregunta ¿Cuál es la regla de tres?


  • Que hace copiando un objeto ¿media?
  • ¿Cuáles son los copia constructor y el operador de asignación de copias?
  • ¿Cuándo debo declararlos yo mismo?
  • ¿Cómo puedo evitar que se copien mis objetos?

1841
2017-11-13 13:27


origen


Respuestas:


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 deberíamos entender qué significa "copiar un objeto" en realidad.

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 el name(name), age(age) parte, esto se llama lista de inicialización de miembros.)

Funciones especiales de miembro

¿Qué significa copiar una person ¿objeto? los main función muestra dos escenarios de copia distintos. La inicialización person b(a); es realizado por el copia constructor. Su trabajo es construir un objeto nuevo basado en el estado de un objeto existente. La asignación b = a es realizado por el operador de asignación de copias. 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 nosotros no declaramos ni el constructor de copia ni el operador de asignación (ni el destructor), estos están definidos implícitamente para nosotros. Cita del estándar:

El constructor de copia y el operador de asignación de copias, [...] y el destructor son funciones especiales de miembros.   [ Nota: La implementación declarará implícitamente estas funciones miembro   para algunos tipos de clase cuando el programa no los declara 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 definido implícitamente para una clase X no sindicalizada realiza una copia de sus subobjetos en forma de miembro.   [n3126.pdf sección 12.8 §16]

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

Definiciones implícitas

Las funciones de miembros especiales implícitamente definidas para person se parece a esto:

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

Copiar de forma paralela es exactamente lo que queremos en este caso: name y age se copian, entonces obtenemos un contenido independiente person objeto. El destructor implícitamente definido siempre está vacío. Esto también está bien en este caso ya que no adquirimos ningún recurso en el constructor. Destructores de los miembros se llaman implícitamente después de la person destructor está terminado:

Después de ejecutar el cuerpo del destructor y destruir cualquier objeto automático asignado 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 deberíamos declarar explícitamente esas funciones especiales para miembros? Cuando nuestra clase maneja un recurso, es decir, cuando un objeto de la clase es responsable para ese recurso. Eso generalmente significa que el recurso es adquirido en el constructor (o pasado al constructor) y liberado en el destructor

Retrocedamos en el tiempo para C ++ pre-estándar. No hubo tal cosa std::string, y los programadores estaban enamorados de los indicadores. los person clase podría haber tenido este aspecto:

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 en día, las personas aún escriben clases de este estilo y se meten en problemas: "Empujé a una persona en un vector y ahora me vuelven locos los errores de memoria!" Recuerde que, de forma predeterminada, copiar un objeto significa copiar sus miembros, pero copiando el name miembro simplemente copia un puntero, no ¡la matriz de caracteres a la que apunta! Esto tiene varios efectos desagradables:

  1. Cambios a través de a se puede observar a través de b.
  2. Una vez b Esta destruido, a.name es un puntero colgante.
  3. Si a se destruye, eliminando los rendimientos del puntero colgando comportamiento indefinido.
  4. Dado que la asignación no tiene en cuenta qué name señaló antes de la asignación, tarde o temprano tendrás fugas de memoria por todo el lugar.

Definiciones explícitas

Como la copia de miembros no tiene el efecto deseado, debemos definir explícitamente el constructor de copias y el operador de asignación de copias para hacer copias en profundidad 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 la inicialización y la asignación: debemos derribar el viejo estado antes de asignar a name para evitar fugas de memoria. Además, tenemos que protegernos contra la autoasignación de la forma x = x. Sin ese control, delete[] name eliminaría la matriz que contiene el fuente cuerda, porque cuando escribes x = x, ambos this->name y that.name contiene el mismo puntero

Seguridad de excepción

Lamentablemente, 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 ocupa de la autoasignación sin una verificación explícita. Una solución aún más robusta a este problema es la copiar e intercambiar idioma, pero no entraré en los detalles de la seguridad de excepciones aquí. Solo mencioné las excepciones para hacer el siguiente punto: Escribir clases que administren recursos es difícil.

Recursos no copiables

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

private:

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

Alternativamente, puede heredar de boost::noncopyable o declararlos como eliminados (C ++ 0x):

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

La regla de tres

A veces necesitas implementar una clase que maneje un recurso. (Nunca administrar múltiples recursos en una sola clase, esto solo provocará dolor). En ese caso, recuerda el regla de tres:

Si necesita declarar explícitamente el destructor,   Copiar constructor o copiar el operador de asignación usted mismo,   es probable que necesites declarar explícitamente los tres.

(Desafortunadamente, esta "regla" no se aplica por el estándar de C ++ o cualquier compilador del que tenga conocimiento).

Consejo

La mayoría de las veces, no necesita administrar un recurso usted mismo, porque una clase existente como std::string ya lo hace por ti. Simplemente compare el código simple usando un std::string miembro a la alternativa intrincada y propensa a errores usando una char* y deberías estar convencido. Mientras se mantenga alejado de los miembros del puntero sin formato, es poco probable que la regla de tres afecte a su propio código.


1512
2017-11-13 13:27



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

Si su clase necesita alguno de

  • un copia constructor,
  • un operador de asignación,
  • o una incinerador de basuras,

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

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

Si no existe una buena semántica para copiar el recurso que maneja 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 versión nueva del estándar C ++ (que es C ++ 11) agrega semántica de movimiento a C ++, lo que probablemente cambie la regla de tres. Sin embargo, sé muy poco acerca de esto para escribir una sección C ++ 11 sobre la Regla de Tres).


450
2017-11-13 14:22



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

Un ejemplo fácil, en lenguaje sencillo, del tipo de problema que resuelve:

Destructor no predeterminado

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

Puede pensar que esto es un trabajo hecho.

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

Una vez, una de estas elimina la memoria en su destructor, la otra tendrá un puntero a la memoria no válida (esto se llama puntero colgante) cuando intente usarlo, las cosas se pondrán peludas.

Por lo tanto, escribe un constructor de copia para que asigne nuevos objetos a sus propias piezas de memoria para destruir.

Operador de asignaciones y constructor de copias

Usted asignó memoria en su constructor a un puntero de 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 de miembro en el nuevo objeto.

Esto significa que el objeto nuevo y el objeto antiguo apuntan a la misma pieza de memoria, de modo que cuando la cambie en un objeto, también se cambiará para el otro objeto. Si un objeto elimina esta memoria, el otro continuará intentando usarlo - eek.

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


134
2018-05-14 14:22



Básicamente, si tiene un destructor (no el destructor predeterminado), significa que la clase que definió tiene cierta asignación de memoria. Supongamos que la clase es utilizada por un 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 tiene solo algunos miembros tipeados 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, es posible que necesitemos un operador de copia profunda, lo que significa que debemos proporcionar un constructor de copia y un operador de asignación.


37
2017-12-31 19:29



¿Qué significa copiar un objeto? Hay algunas formas en que puede copiar objetos; hablemos de los dos tipos a los que probablemente se está refiriendo: copia profunda y copia superficial.

Como estamos en un lenguaje orientado a objetos (o al menos lo estamos asumiendo), digamos que tiene asignada una parte de la memoria. Dado que es un lenguaje OO, podemos referirnos fácilmente a trozos de memoria que asignamos porque generalmente son variables primitivas (ints, chars, bytes) o clases definidas que están hechas de nuestros propios tipos y primitivas. Entonces digamos que tenemos una clase de automóvil 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 juegos 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 destinado a compartir la memoria real de la que está hecho car1. (Por lo general, es un error hacer esto y en clases suele ser la manta de la que se trata.) Pretenda que cada vez que pregunte por car2, realmente está resolviendo un puntero al espacio de memoria de car1 ... eso es más o menos lo que 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.*/

Por lo tanto, independientemente del idioma en el que escriba, tenga mucho cuidado con lo que quiere decir cuando se trata de copiar objetos porque la mayoría de las veces quiere una copia en profundidad.

¿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 copia cuando escribe código como Car car2 = car1;  Básicamente, si declaras una variable y la asignas en una línea, es cuando se llama al constructor de copia. El operador de asignación es lo que sucede cuando usas un signo igual--car2 = car1;. darse cuenta car2 no está declarado en la misma declaración. Los dos trozos de código que escribe para estas operaciones son 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 de que 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 producción de alguna manera, solo necesita declararlos cuando los necesite. Debe saber qué hace su lenguaje de programa si elige usarlo 'por accidente' y no lo hizo, es decir. obtienes el compilador predeterminado. Rara vez uso constructores de copia, por ejemplo, pero las anulaciones de operador de asignación son muy comunes. ¿Sabía que puede anular también lo que suman, restan, 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 las personas las copien, puede hacerlo público y alertar al programador lanzando una excepción y también no copiando el objeto.


27
2017-10-17 16:37



¿Cuándo debo declararlos yo mismo?

La Regla de los Tres establece que si declara alguno de

  1. copia constructor
  2. operador de asignación de copias
  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 provenía de la clase que realizaba algún tipo de gestión de recursos, y eso casi siempre implicaba que

  • cualquiera que sea la gestión de recursos que se estaba haciendo en una operación de copia probablemente deba hacerse en la otra operación de copia y

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

Una consecuencia de la Regla de los Tres es que la presencia de un destructor declarado por el usuario indica que es poco probable que la copia simple de miembro sea apropiada para las operaciones de copiado 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 apreció 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 la restricción de 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?

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

19
2018-01-12 09:54



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 de la semántica de movimiento 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


9
2018-01-07 05:38



La regla de tres en C ++ es un principio fundamental del diseño y el desarrollo de tres requisitos: si hay una definición clara en una de las siguientes funciones de miembro, entonces el programador debe definir las otras dos funciones de los miembros juntas. A saber, 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 usa 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;

5
2017-08-12 04:27