Buscar..


Regla de cinco

C ++ 11

C ++ 11 introduce dos nuevas funciones miembro especiales: el constructor de movimientos y el operador de asignación de movimientos. Por las mismas razones por las que desea seguir la Regla de tres en C ++ 03, por lo general, debe seguir la Regla de cinco en C ++ 11: si una clase requiere UNA de las cinco funciones miembro especiales y si mueve la semántica son deseados, entonces lo más probable es que requiera TODOS CINCO de ellos.

Sin embargo, tenga en cuenta que no seguir la Regla de cinco no suele considerarse un error, sino una oportunidad de optimización perdida, siempre que se siga la Regla de tres. Si no hay un constructor de movimientos o un operador de asignación de movimientos disponible cuando el compilador normalmente usaría uno, en su lugar utilizará la semántica de copia, de ser posible, lo que resultará en una operación menos eficiente debido a operaciones de copia innecesarias. Si la semántica de movimiento no se desea para una clase, entonces no es necesario declarar un constructor de movimiento u operador de asignación.

El mismo ejemplo que para la Regla de los Tres:

class Person
{
    char* name;
    int age;

public:
    // Destructor 
    ~Person() { delete [] name; }

    // Implement Copy Semantics
    Person(Person const& other)
        : name(new char[std::strlen(other.name) + 1])
        , age(other.age)
    {
        std::strcpy(name, other.name);
    }
    
    Person &operator=(Person const& other) 
    {
        // Use copy and swap idiom to implement assignment.
        Person copy(other);
        swap(*this, copy);
        return *this;
    }

    // Implement Move Semantics
    // Note: It is usually best to mark move operators as noexcept
    //       This allows certain optimizations in the standard library
    //       when the class is used in a container.

    Person(Person&& that) noexcept
        : name(nullptr)               // Set the state so we know it is undefined
        , age(0)
    {
        swap(*this, that);
    }

    Person& operator=(Person&& that) noexcept
    {
        swap(*this, that);
        return *this;
    }

    friend void swap(Person& lhs, Person& rhs) noexcept
    {
        std::swap(lhs.name, rhs.name);
        std::swap(lhs.age, rhs.age);
    }
};

Alternativamente, tanto el operador de asignación de copia como el de movimiento pueden reemplazarse con un operador de asignación única, que toma una instancia por valor en lugar de referencia o rvalue de referencia para facilitar el uso del lenguaje de copia e intercambio.

Person& operator=(Person copy)
{
    swap(*this, copy);
    return *this;
}

La extensión de la Regla de tres a la Regla de cinco es importante por razones de rendimiento, pero no es estrictamente necesaria en la mayoría de los casos. Agregar el constructor de copia y el operador de asignación garantiza que mover el tipo no perderá memoria (la construcción de movimientos simplemente volverá a copiarse en ese caso), pero realizará copias que la persona que llama probablemente no anticipó.

Regla de cero

C ++ 11

Podemos combinar los principios de la Regla de cinco y RAII para obtener una interfaz mucho más sencilla: la Regla de cero: cualquier recurso que deba administrarse debe ser de su propio tipo. Ese tipo tendría que seguir la Regla de los Cinco, pero todos los usuarios de ese recurso no necesitan escribir ninguna de las cinco funciones de miembro especiales y simplemente pueden default todas ellas.

Usando la clase de Person introducida en el ejemplo de la Regla de tres , podemos crear un objeto de administración de recursos para los cstrings :

class cstring {
private:
    char* p;

public:
    ~cstring() { delete [] p; }
    cstring(cstring const& );
    cstring(cstring&& );
    cstring& operator=(cstring const& );
    cstring& operator=(cstring&& );

    /* other members as appropriate */
};

Y una vez que esto está separado, nuestra clase Person vuelve mucho más simple:

class Person {
    cstring name;
    int arg;

public:
    ~Person() = default;
    Person(Person const& ) = default;
    Person(Person&& ) = default;
    Person& operator=(Person const& ) = default;
    Person& operator=(Person&& ) = default;

    /* other members as appropriate */
};

Los miembros especiales en Person ni siquiera necesitan ser declarados explícitamente; el compilador los tomará de forma predeterminada o los eliminará de manera adecuada, según el contenido de la Person . Por lo tanto, el siguiente es también un ejemplo de la regla de cero.

struct Person {
    cstring name;
    int arg;
};

Si el cstring fuera un tipo de solo movimiento, con un constructor de delete / copia / operador de asignación, la Person también se movería automáticamente solo.

El término regla de cero fue introducido por R. Martinho Fernandes

Regla de tres

c ++ 03

La Regla de los Tres establece que si un tipo necesita tener un constructor de copia, operador de asignación de copia o destructor definido por el usuario, entonces debe tener los tres .

El motivo de la regla es que una clase que necesita cualquiera de los tres administra algún recurso (manejadores de archivos, memoria asignada dinámicamente, etc.), y los tres son necesarios para administrar ese recurso de manera consistente. Las funciones de copia tratan sobre cómo el recurso se copia entre los objetos, y el destructor destruiría el recurso, de acuerdo con los principios de RAII .

Considere un tipo que administra un recurso de cadena:

class Person
{
    char* name;
    int age;

public:
    Person(char const* new_name, int new_age)
        : name(new char[std::strlen(new_name) + 1])
        , age(new_age)
    {
       std::strcpy(name, new_name);
    }

    ~Person() {
        delete [] name;
    }
};

Dado que el name fue asignado en el constructor, el destructor lo desasigna para evitar pérdidas de memoria. ¿Pero qué pasa si se copia ese objeto?

int main()
{
    Person p1("foo", 11);
    Person p2 = p1;
}

Primero, se construirá p1 . Entonces p2 se copiará de p1 . Sin embargo, el constructor de copia generado por C ++ copiará cada componente del tipo tal como está. Lo que significa que p1.name y p2.name apuntan a la misma cadena.

Cuando finalice el main , se llamará a los destructores. El primer destructor de p2 será llamado; se eliminará la cadena. Entonces se llamará al destructor de p1 . Sin embargo, la cadena ya está eliminada . Llamar a delete en la memoria que ya se eliminó produce un comportamiento indefinido.

Para evitar esto, es necesario proporcionar un constructor de copia adecuado. Un enfoque es implementar un sistema de referencia de recuento, donde diferentes instancias de Person comparten la misma cadena de datos. Cada vez que se realiza una copia, se incrementa el recuento de referencias compartidas. El destructor luego disminuye el conteo de referencia, liberando solo la memoria si el conteo es cero.

O podríamos implementar semántica de valor y comportamiento de copia profunda :

Person(Person const& other)
    : name(new char[std::strlen(other.name) + 1])
    , age(other.age)
{
    std::strcpy(name, other.name);
}

Person &operator=(Person const& other) 
{
    // Use copy and swap idiom to implement assignment
    Person copy(other);
    swap(copy);            //  assume swap() exchanges contents of *this and copy
    return *this;
}

La implementación del operador de asignación de copia se complica por la necesidad de liberar un búfer existente. La técnica de copia e intercambio crea un objeto temporal que contiene un nuevo búfer. Al intercambiar el contenido de *this y copy otorga la propiedad de la copy del búfer original. La destrucción de la copy , a medida que la función regresa, libera el búfer que anteriormente era propiedad de *this .

Protección de autoasignación

Al escribir un operador de asignación de copia, es muy importante que pueda trabajar en caso de autoasignación. Es decir, tiene que permitir esto:

SomeType t = ...;
t = t;

La autoasignación usualmente no ocurre de una manera tan obvia. Normalmente ocurre a través de una ruta tortuosa a través de varios sistemas de códigos, donde la ubicación de la asignación simplemente tiene dos punteros o referencias Person y no tiene idea de que son el mismo objeto.

Cualquier operador de asignación de copia que escriba debe poder tener esto en cuenta.

La forma típica de hacerlo es envolver toda la lógica de asignación en una condición como esta:

SomeType &operator=(const SomeType &other)
{
    if(this != &other)
    {
        //Do assignment logic.
    }
    return *this;
}

Nota: es importante pensar en la autoasignación y asegurarse de que su código se comporte correctamente cuando suceda. Sin embargo, la autoasignación es un caso muy raro y la optimización para evitarlo puede en realidad pesarse sobre el caso normal. Dado que el caso normal es mucho más común, el pesimismo para la autoasignación puede reducir la eficiencia de su código (así que tenga cuidado al usarlo).

Como ejemplo, la técnica normal para implementar el operador de asignación es el copy and swap idiom . La implementación normal de esta técnica no se molesta en probar la autoasignación (aunque la autoasignación es costosa porque se hace una copia). La razón es que la pesimización del caso normal ha demostrado ser mucho más costosa (ya que ocurre con más frecuencia).

c ++ 11

Los operadores de asignación de movimientos también deben estar protegidos contra la autoasignación. Sin embargo, la lógica de muchos de estos operadores se basa en std::swap , que puede manejar el intercambio de / a la misma memoria. Entonces, si su lógica de asignación de movimientos no es más que una serie de operaciones de intercambio, entonces no necesita protección de autoasignación.

Si este no es el caso, debe tomar medidas similares a las anteriores.



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow