c++ - ¿Cuál es la regla de tres?




copy-constructor assignment-operator (6)

¿Cuándo necesito declararlos yo mismo?

La Regla de los Tres establece que si declara alguno de

  1. copia constructor
  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 debía a 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 realizando 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 administración del recurso (generalmente liberándolo). El recurso clásico que se administró fue la memoria, y esta es la razón por la que 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": ambas 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 poco probable que la copia simple de miembros 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 el código heredado.

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

Declarar constructor de copia y 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 el constructor de copia y el operador de asignación 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);
}
  • ¿Qué significa copiar un objeto ?
  • ¿Qué son el constructor de copia y el operador de asignación de copia ?
  • ¿Cuándo necesito declararlos yo mismo?
  • ¿Cómo puedo evitar que se copien mis objetos?

Introducción

C ++ trata variables de tipos definidos por el usuario con semántica de valor . Esto significa que los objetos se copian implícitamente en diversos 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 parte del name(name), age(age) , esto se llama una lista de inicialización de miembros ).

Funciones especiales para miembros

¿Qué significa copiar un objeto de person ? La función main muestra dos escenarios de copia distintos. La person b(a); inicialización person b(a); Es realizada por el constructor de copia . Su trabajo es construir un objeto nuevo 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 es generalmente un poco más complicado, porque el objeto de destino ya se encuentra en un estado válido que debe ser tratado.

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

El constructor de [...] copias y el operador de asignación de copias, [...] 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 definido de manera implícita para una clase X no sindicalizada realiza una copia de sus subobjetos a nivel de miembro. [n3126.pdf sección 12.8 §16]

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

Definiciones implícitas

Las funciones de miembro especiales definidas implícitamente para person tienen 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 de miembros es exactamente lo que queremos en este caso: el name y la age se copian, por lo que obtenemos un objeto de person autónoma e independiente. El destructor definido implícitamente está siempre 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 la person destructor haya terminado:

Después de ejecutar el cuerpo del destructor y de 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]

Gestión de recursos

Entonces, ¿cuándo debemos declarar explícitamente esas funciones miembro especiales? Cuando nuestra clase administra 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 ++ estándar. No había tal cosa como std::string , y los programadores estaban enamorados de los punteros. La clase de person 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 en día, la gente todavía escribe clases con este estilo y se mete en problemas: " ¡Empujé a una persona a un vector y ahora tengo errores de memoria locos! " Recuerda que, de forma predeterminada, copiar un objeto significa copiar sus miembros, pero copiar el miembro name simplemente copia un puntero, no la matriz de caracteres a la que apunta! Esto tiene varios efectos desagradables:

  1. Los cambios a través de a se pueden observar a través de b .
  2. Una vez que se destruye b , a.name es un puntero colgante.
  3. Si se destruye a, la eliminación del puntero colgante produce un comportamiento indefinido .
  4. Dado que la asignación no tiene en cuenta a qué name apuntó antes de la asignación, tarde o temprano tendrá pérdidas de memoria por todas partes.

Definiciones explicitas

Dado que la copia de miembros no tiene el efecto deseado, debemos definir el constructor de copia y el operador de asignación de copia explícitamente 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;
}

Note 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, tenemos que proteger contra la autoasignación de la forma x = x . Sin esa comprobación, delete[] name eliminaría 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 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 un control explícito. Una solución aún más sólida para este problema es el lenguaje de copia e intercambio , pero no voy a entrar en los detalles de seguridad de excepción aquí. Solo mencioné las excepciones para hacer el siguiente punto: escribir las clases que administran los recursos es difícil.

Recursos no copiables

Algunos recursos no pueden o no deben ser copiados, como los manejadores de archivos o los mutex. En ese caso, simplemente declare el constructor de copia y el operador de asignación de copia 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 es necesario implementar una clase que administra un recurso. (Nunca administre múltiples recursos en una sola clase, esto solo llevará al dolor). En ese caso, recuerde la regla de tres :

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

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

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 usted. Simplemente compare el código simple usando un miembro std::string a la alternativa complicada y propensa a errores usando un char* y debe estar convencido. Mientras se mantenga alejado de los miembros de puntero en bruto, es poco probable que la regla de tres se refiera a su propio código.


Básicamente, si tiene un destructor (no el destructor predeterminado) significa que la clase que definió tiene alguna asignación de memoria. Supongamos que la 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 tipográficos primitivos, un operador de asignación predeterminado funcionaría, pero si tiene algunos miembros punteros 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.


La Regla de los Tres es una regla de oro para C ++, básicamente dice

Si tu clase necesita alguno de

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

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

Las razones de esto es que los tres se usan generalmente para administrar un recurso, y si su clase administra un recurso, por lo general necesita administrar la copia y la liberación.

Si no hay una buena semántica para copiar el recurso que administra su clase, entonces considere prohibir la copia declarando (no defining ) 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 movimiento semántico a C ++, lo que probablemente cambiará la Regla de Tres. Sin embargo, sé muy poco sobre esto para escribir una sección de C ++ 11 sobre la Regla de los Tres.)


La regla de tres en C ++ es un principio fundamental del diseño y el desarrollo de tres requisitos que, si existe una definición clara en una de las siguientes funciones miembro, el programador debe definir las otras dos funciones miembros juntos. 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 utiliza 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;

Muchas de las respuestas existentes ya tocan el constructor de copia, el operador de asignación y el destructor. Sin embargo, en la publicación C ++ 11, la introducción de 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





rule-of-three