¿Cuál es la regla de tres?

  • ¿Qué significa copiar un objeto?
  • ¿Qué es un constructor de copia y una declaración de asignación de copia?
  • ¿Cuándo necesito declararlos yo mismo?
  • ¿Cómo puedo evitar copiar mis objetos?
1911
13 нояб. establecido por fredoverflow el 13 de noviembre 2010-11-13 16:27 '10 a las 16:27 2010-11-13 16:27
@ 8 respuestas

Introducción

C ++ procesa variables de tipo personalizado con valores semánticos. Esto significa que los objetos se copian implícitamente en diferentes contextos, y debemos entender lo que realmente significa "copiar un objeto".

Considere un ejemplo simple:

 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) , esto se llama la lista de iniciadores miembros ).

Funciones especiales para miembros

¿Qué significa copiar a una person ? La función main muestra dos guiones de copia diferentes. person b(a); inicialización person b(a); ejecutado por el constructor de copia. Su tarea es construir un nuevo objeto basado en el estado de un objeto existente. La asignación b = a es realizada por el operador de asignación de copia. Su trabajo suele ser un poco más difícil, porque el objeto objetivo ya se encuentra en un estado aceptable que debe ser tratado.

Dado que no hemos declarado ni el constructor de copia ni el operador de asignación (o destructor), están definidos de forma implícita para nosotros. Cita de la norma:

El constructor de copia [...] y el operador de asignación de copia, [...] y el destructor son funciones miembro especiales. [Nota: una implementación declarará implícitamente estas funciones miembro para ciertos tipos de clases 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 elementos:

Un constructor de copia definido de forma implícita para una clase X no unitaria realiza una copia en fase de sus subobjetos. [n3126.pdf sección 12.8 §16]

Un operador de asignación de copia asignado de forma implícita para una clase X no unitaria realiza una asignación por fases de una copia de sus subobjetos. [n3126.pdf sección 12.8 §30]

Definiciones implícitas

Las funciones de miembro especiales definidas implícitamente para una person son las siguientes:

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

En este caso, queremos copiar lo que queremos: el name y la age copian, por lo que obtenemos un objeto de person independiente e independiente. Un destructor definido implícitamente está siempre vacío. Esto también es excelente en este caso, ya que no obtuvimos ningún recurso en el constructor. Se llama implícitamente a los destructores miembros después de que la person completado el destructor:

Después de ejecutar el cuerpo del destructor y destruir cualquier objeto automático asignado en el cuerpo, el destructor de la clase X causa destructores para los miembros directos de X [n3126.pdf 12.4 §6]

Gestión de recursos

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

Volvamos a la norma preliminar C ++. No había tal cosa como std::string , y los programadores estaban enamorados de los punteros. La clase de person podría verse 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 en día, las personas siguen escribiendo en este estilo y se meten 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 sus elementos, pero copiar el miembro del name simplemente copia el puntero, no la matriz de caracteres a la que apunta. Esto tiene varios efectos desagradables:

  • Los cambios a través de a se pueden observar a través de b .
  • Tan pronto como b destruye b , a.name es un puntero a.name .
  • Si a destruye a, la eliminación del puntero roto da un comportamiento indefinido .
  • Como la asignación no tiene en cuenta el name especificado antes de la asignación, tarde o temprano obtendrá pérdidas de memoria en todas partes.

Definiciones explícitas

Dado que copiar en orden no tiene el efecto deseado, debemos definir explícitamente el constructor de copia y el operador de asignación de copia para crear 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 !=  { 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; } 

Observe la diferencia entre la inicialización y la asignación: debemos eliminar el estado anterior antes de asignar un name para evitar pérdidas de memoria. Además, debemos proteger la forma x = x de la autoconciencia. Sin esta comprobación, delete[] name eliminará la matriz que contiene la cadena de origen, porque cuando escribe x = x , tanto this->name como that.name contienen el mismo puntero.

Seguridad de excepción

Desafortunadamente, esta solución fallará si el new char[...] lanza una excepción debido al agotamiento de la memoria. Una posible solución es introducir una variable local y cambiar el orden de los operadores:

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

También proporciona autoasignación sin verificación explícita. Una solución aún más confiable para este problema es el lenguaje de copiar e intercambiar , pero no entraré en los detalles de las excepciones de seguridad. Solo mencioné las excepciones para hacer lo siguiente: Escribir las clases que administran los recursos es difícil.

Recursos no copiables

Algunos recursos no pueden o no deben copiarse, por ejemplo, descriptores de archivos o mutex. En este caso, simplemente declare el constructor de copia y el operador de destino de copia como private sin especificar 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; 

Regla de tres

A veces es necesario implementar una clase que administra un recurso. (Nunca administre múltiples recursos en la misma clase, solo llevará al dolor). En este caso, recuerde la regla de tres :

Si necesita declarar explícitamente un destructor, copiar el constructor o copiar la instrucción de asignación, probablemente deba declarar explícitamente los tres.

(Desafortunadamente, esta "regla" no es aplicada por el estándar de C ++ o el compilador, que yo sepa).

El tablero

En la mayoría de los casos, no necesita administrar el recurso usted mismo, porque una clase existente, como std::string , ya lo hace por usted. Simplemente compare el código simple con el miembro std::string con una alternativa confusa y propensa a errores usando char* , y debe estar seguro. Mientras te mantengas alejado de los punteros en bruto, la regla de tres difícilmente se aplica a tu propio código.

1561
13 нояб. la respuesta es dada por fredoverflow 13 de noviembre 2010-11-13 16:27 '10 a las 16:27 2010-11-13 16:27

La regla de tres es una regla de oro para C ++, generalmente hablando

Si tu clase necesita alguno de

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

se determina explícitamente, entonces probablemente se requerirán los tres .

Las razones de esto son que los tres se usan generalmente para administrar el recurso, y si su clase administra el recurso, por lo general necesita administrar la copia y liberarla.

Si no hay una buena semántica para copiar los recursos que administra su clase, entonces considere prohibir la copia declarando (no definiendo ) el constructor de la 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 la semántica de movimientos en C ++, que probablemente cambiará la regla de tres. Sin embargo, sé muy poco sobre esto para escribir C + +11 seccion en Regla de Tres.)

467
13 нояб. La respuesta es dada por sbi el 13 de noviembre. 2010-11-13 17:22 '10 a las 17:22 2010-11-13 17:22

La ley de los tres grandes es como se dijo anteriormente.

Un ejemplo fácil, en un lenguaje sencillo, del problema que él resuelve:

Destructor personalizado

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

Podrías pensar que esto es un trabajo.

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

Tan pronto como uno de ellos elimine la memoria en su destructor, el otro tendrá un puntero a un recuerdo no válido (esto se llama un puntero colgante), cuando intenta usarlo, todo se verá peludo.

En consecuencia, está escribiendo un constructor de copia para que asigne nuevos objetos para destruir sus propios fragmentos de memoria.

Operador de asignación y constructor de copia.

Usted asignó memoria en su constructor a un puntero a un miembro de su clase. Cuando copia un objeto de esta clase, la declaración de asignación predeterminada y el constructor de copia copiarán el valor de este elemento puntero al nuevo objeto.

Esto significa que el objeto nuevo y el objeto antiguo apuntarán a la misma parte de la memoria, de modo que cuando lo cambie en un objeto, se cambiará por otro objeto objeto. Si un objeto elimina esta memoria, otro continuará intentando usarla: eek.

Para resolver este problema, escriba su propia versión del constructor de copia y el operador de asignación. Sus versiones asignan memoria separada a nuevos objetos y copian los valores apuntados por el primer puntero, no su dirección.

143
14 мая '12 в 17:22 2012-05-14 17:22 Respondió a Stefan el 14 de mayo de '12 a las 5:22 p.m 2012/2012-14 17:22

Básicamente, si tiene un destructor (y no un destructor predeterminado), esto significa que la clase que definió tiene alguna asignación de memoria. Supongamos que una clase se usa afuera 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 tipificados primitivos, se ejecutará la instrucción de asignación predeterminada, pero si tiene algunos elementos de puntero y objetos que no tienen instrucciones de asignación, el resultado será impredecible. Por lo tanto, podemos decir que si hay algo que eliminar en la clase destructor, 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.

39
31 дек. respuesta dada fatma.ekici 31 dic. 2012-12-31 22:29 '13 a las 22:29 2012-12-31 22:29

¿Qué significa copiar un objeto? Hay varias formas de copiar objetos. Díganos sobre dos tipos a los que probablemente se refiere: una copia profunda y una copia superficial.

Ya que estamos en un lenguaje orientado a objetos (o al menos supongamos que es así), digamos que tiene una memoria dedicada. Dado que este es un lenguaje OO, podemos referirnos fácilmente a las piezas de memoria que asignamos, ya que generalmente son variables primitivas (caracteres, caracteres, bytes) o clases que hemos definido y que están hechas de nuestros propios tipos y primitivas. Entonces, digamos que tenemos una clase de autos como sigue:

 class Car //A very simple class just to demonstrate what these definitions mean. //It 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 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  // Copy Constructor { this.sPrintColor = other.sPrintColor; this.sModel = other.sModel; this.sMake = other.sMake; } public Car  =(const Car  // Assignment Operator { if(this !=  { 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 dos objetos en 2 conjuntos de memoria completos.

 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 vamos a hacer algo raro. Dejemos que se diga que car2 está programado incorrectamente o intencionalmente para intercambiar la memoria real desde la cual se genera car1. (Esto suele ser un error, y en las clases suele ser la manta que se mencionó). Imagina que en cualquier momento cuando preguntas sobre car2, realmente decides el puntero al espacio de memoria de car1 ... que es más o menos tan pequeño hay una copia

 //Shallow copy example //Assume we're in C++ because it standard behavior is to shallow copy objects if you do not have a constructor written for an operation. //Now let 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; car1.changePaint("red"); 

Por lo tanto, no importa qué idioma escriba, tenga mucho cuidado con lo que quiere decir al copiar objetos, ya que la mayoría de las veces desea obtener una copia profunda.

¿Qué es un constructor de copia y una declaración de asignación de copia? Ya los he usado arriba. El constructor de copia se invoca cuando se ingresa un código como Car car2 = car1; Esencialmente, si declara una variable y la asigna en una línea, cuando llame al constructor de copia. El operador de asignación es lo que sucede cuando usas el signo igual - car2 = car1; . La notificación car2 no se anuncia en la misma declaración. Las dos piezas de código que escribes para estas operaciones probablemente sean muy similares. De hecho, en un patrón de diseño típico, hay otra función a la que llama para configurar todo, una vez que esté satisfecho con la copia / asignación inicial, es legal: si observa el código que escribí, las funciones son casi idénticas.

¿Cuándo necesito declararlos yo mismo? Si no está escribiendo el código que necesita ser compartido o producido de alguna manera, realmente solo necesita declararlos cuando los necesite. Debe saber qué hace su lenguaje de programación, si decide utilizarlo "por casualidad" y no lo ha hecho, es decir, obtienes el compilador por defecto. Por ejemplo, rara vez utilizo constructores de copia, pero los operadores de asignación de reemplazo son muy comunes. ¿Sabe que puede redefinir lo que significa sumar, restar, etc.?

¿Cómo puedo evitar copiar mis objetos? Un comienzo sensato es anular todas las formas en que puede asignar memoria para su objeto utilizando una función privada. Si realmente no desea que las personas los copien, puede ponerlo a disposición del público y advertir al programador lanzando una excepción y no copiando el objeto.

31
17 окт. Respuesta dada por el usuario1701047 17 oct. 2012-10-17 19:37 '12 a las 7:37 pm 2012-10-17 19:37

¿Cuándo necesito declararlos yo mismo?

La regla de los tres estados establece que si declara alguno de

  • Copiar declaración de asignación de constructor
  • destructor

entonces debes declarar los tres. Desde el hecho de que la necesidad de usar el significado de la operación de copia casi siempre se deriva de la clase que realiza algún tipo de gestión de recursos, y que casi siempre significa que

  • cualquier gestión de recursos se realizó en una operación de copia, puede haber sido necesario realizar operaciones de copia en otra, y

  • El destructor de clases también participará en la administración de recursos (generalmente liberándolo). El recurso de administración clásico era la memoria, y es por eso que todas las clases de biblioteca estándar que (por ejemplo, los contenedores STL que realizan la administración de memoria dinámica) declaran las "tres grandes": ambas operaciones de copia y destructor.

La consecuencia de la regla tres es que la presencia de un destructor declarado por el usuario indica que una copia simple del miembro no es adecuada para las operaciones de copia en la clase. Esto, 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án lo correcto. En el momento en que se adoptó C ++ 98, el valor de esta línea de razonamiento no se apreciaba completamente, por lo tanto, en C ++ 98, la existencia de un destructor declarado por el usuario no afectó la preparación de los compiladores para generar operaciones de copia. Este sigue siendo el caso en C ++ 11, pero solo porque restringir las condiciones bajo las cuales se realizan las operaciones de copia romperá demasiado el código obsoleto.

¿Cómo puedo evitar copiar mis objetos?

Declare un constructor de copia y una declaración de asignación de copia como un 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, también puede declarar que el constructor de copia y el operador de asignación se eliminaron.

 class MemoryBlock { public: MemoryBlock(const MemoryBlock other) = delete // Copy assignment operator. MemoryBlock operator=(const MemoryBlock other) =delete }; int main() { MemoryBlock a; MemoryBlock b(a); } 
21
12 янв. La respuesta es dada por Ajay yadav 12 de enero. 2016-01-12 12:54 '16 a las 12:54 2016-01-12 12:54