¿Qué es la regla de tres?

Resuelto fredoverflow asked hace 13 años • 8 respuestas
  • ¿Qué significa copiar un objeto? ?
  • ¿Qué son el constructor de copia y el operador de asignación de copia ? ?
  • ¿Cuándo tengo que declararlos yo mismo?
  • ¿Cómo puedo evitar que se copien mis objetos?
fredoverflow avatar Nov 13 '10 20:11 fredoverflow
Aceptado

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 varios contextos y debemos comprender qué significa realmente "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 la parte le desconcierta name(name), age(age), esto se denomina lista de inicializadores de miembros ).

Funciones especiales de los miembros

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

Dado que no declaramos ni el constructor de copia ni el operador de asignación (ni el destructor), estos están implícitamente definidos para nosotros. Cita de la norma:

El constructor de [...] copia, 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 declara explícitamente. La implementación los definirá implícitamente si se utilizan. [...] 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 por miembros 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 una asignación de copia por miembros de sus subobjetos. [n3126.pdf sección 12.8 §30]

Definiciones implícitas

Las funciones miembro especiales definidas implícitamente tienen personeste 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 por miembros es exactamente lo que queremos en este caso: namey agese copian, por lo que obtenemos un personobjeto independiente y autónomo. 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 llaman implícitamente una vez personfinalizado el destructor:

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]

Gestionar recursos

Entonces, ¿cuándo deberíamos 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. Por lo general, eso significa que el recurso se adquiere en el constructor (o se pasa al constructor) y se libera en el destructor.

Retrocedamos en el tiempo hasta el C++ preestándar. No existían los punteros std::stringy 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 en día, 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 sus miembros, pero copiar el namemiembro simplemente copia un puntero, no la matriz de caracteres a la que apunta. Esto tiene varios efectos desagradables:

  1. Los cambios vía ase pueden observar vía b.
  2. Una vez bdestruido, a.namequeda un puntero colgando.
  3. Si ase destruye, eliminar el puntero colgante produce un comportamiento indefinido .
  4. Dado que la tarea no tiene en cuenta lo que namese señaló antes de la tarea, tarde o temprano tendrás pérdidas de memoria por todas partes.

Definiciones explícitas

Dado que la copia por miembros no tiene el efecto deseado, debemos definir explícitamente el constructor de copia y el operador de asignación de copia para realizar 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 eliminar el estado anterior antes de asignarlo para nameevitar pérdidas de memoria. Además, tenemos que protegernos contra la autoasignación del formulario x = x. Sin esa verificación, delete[] nameeliminaría la matriz que contiene la cadena fuente , porque cuando escribe x = x, ambas this->namecontienen that.nameel mismo puntero.

Seguridad excepcional

Desafortunadamente, esta solución fallará si new char[...]genera 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 sólida a este problema es el modismo copiar e intercambiar , pero no entraré en detalles sobre la seguridad de las excepciones aquí. Sólo mencioné excepciones para aclarar el siguiente punto: escribir clases que administren recursos es difícil.

Recursos no copiables

Algunos recursos no pueden o no deben copiarse, como identificadores de archivos o mutex. En ese caso, simplemente declare el constructor de copia y el operador de asignación de copia 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 es necesario implementar una clase que administre un recurso. (Nunca administre varios recursos en una sola clase, esto solo resultará doloroso). 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, probablemente necesite declarar explícitamente los tres.

(Desafortunadamente, esta "regla" no la aplica el estándar C++ ni ningún compilador que yo sepa).

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. El gobierno de cinco estados también implementará estas funciones.

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;                // 1/5: Copy Ctor
    person(person &&) noexcept = default;            // 4/5: Move Ctor
    person& operator=(const person &) = default;     // 2/5: Copy Assignment
    person& operator=(person &&) noexcept = default; // 5/5: Move Assignment
    ~person() noexcept = default;                    // 3/5: Dtor
};

La regla del cero

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

Consejo

La mayoría de las veces, no necesita administrar un recurso usted mismo, porque una clase existente std::stringya lo hace por usted. Simplemente compare el código simple que usa un std::stringmiembro con la alternativa complicada y propensa a errores que usa un char*y quedará convencido. Mientras se mantenga alejado de los miembros punteros sin formato, es poco probable que la regla de tres afecte a su propio código.

fredoverflow avatar Nov 13 '2010 13:11 fredoverflow

La Regla de Tres es una regla general para C++ y básicamente dice

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 necesite 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 además de la liberación.

Si no existe 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 de C++11. sobre la Regla de Tres.)

sbi avatar Nov 13 '2010 14:11 sbi

La ley de los tres grandes es la especificada anteriormente.

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

Destructor no predeterminado

Asignaste memoria en tu constructor y por eso necesitas escribir un destructor para eliminarla. De lo contrario, provocará una pérdida de memoria.

Se podría pensar que esto 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 que uno de estos elimina la memoria en su destructor, el otro tendrá un puntero a una memoria no válida (esto se llama puntero colgante) cuando intente usarlo, las cosas se pondrán complicadas.

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

Operador de asignación y constructor de copias.

Asignaste memoria en tu constructor a un puntero miembro de tu clase. Cuando copia 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 al nuevo objeto.

Esto significa que el objeto nuevo y el objeto antiguo apuntarán al mismo fragmento de memoria, por lo que cuando lo cambies en un objeto, también se cambiará para el otro objeto. Si un objeto elimina esta memoria, el otro seguirá intentando utilizarla - eek.

Para resolver esto, escriba su propia versión del constructor de copia y del 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 avatar May 14 '2012 14:05 Stefan

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 externamente 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 tiene sólo algunos miembros primitivos escritos, 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, 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.

fatma.ekici avatar Dec 31 '2012 19:12 fatma.ekici