The rule of three/five/zero
Contenidos |
Regla de tres
Si una clase requiere un destructor definido por el usuario, un copy constructor definido por el usuario, o un copy assignment operator definido por el usuario, casi seguramente requiere los tres.
Debido a que C++ copia y copia-asigna objetos de tipos definidos por el usuario en varias situaciones (pasar/devolver por valor, manipular un contenedor, etc), estas funciones miembro especiales serán llamadas, si son accesibles, y si no están definidas por el usuario, son implícitamente-definidas por el compilador.
Las funciones miembro especiales definidas implícitamente no deben usarse si la clase gestiona un recurso cuyo manejador es un objeto de tipo no-clase (puntero crudo, descriptor de archivo POSIX, etc), cuyo destructor no hace nada y el constructor de copia/operador de asignación realiza una "copia superficial" (copia el valor del manejador, sin duplicar el recurso subyacente).
#include <cstddef> #include <cstring> #include <iostream> #include <utility> class rule_of_three { char* cstring; // raw pointer used as a handle to a // dynamically-allocated memory block public: explicit rule_of_three(const char* s = "") : cstring(nullptr) { if (s) { cstring = new char[std::strlen(s) + 1]; // allocate std::strcpy(cstring, s); // populate } } ~rule_of_three() // I. destructor { delete[] cstring; // deallocate } rule_of_three(const rule_of_three& other) // II. copy constructor : rule_of_three(other.cstring) {} rule_of_three& operator=(const rule_of_three& other) // III. copy assignment { // implemented through copy-and-swap for brevity // note that this prevents potential storage reuse rule_of_three temp(other); std::swap(cstring, temp.cstring); return *this; } const char* c_str() const // accessor { return cstring; } }; int main() { rule_of_three o1{"abc"}; std::cout << o1.c_str() << ' '; auto o2{o1}; // II. uses copy constructor std::cout << o2.c_str() << ' '; rule_of_three o3("def"); std::cout << o3.c_str() << ' '; o3 = o2; // III. uses copy assignment std::cout << o3.c_str() << '\n'; } // I. all destructors are called here
Salida:
abc abc def abc
Las clases que gestionan recursos no copiables mediante manejadores copiables pueden tener que declarar la asignación de copia y el constructor de copia como private y no proporcionar sus definiciones (hasta C++11) definir la asignación de copia y el constructor de copia como = delete (desde C++11) . Esta es otra aplicación de la regla de tres: eliminar uno y dejar el otro definido implícitamente es típicamente incorrecto.
Regla de cinco
Debido a que la presencia de un destructor definido por el usuario (incluyendo = default o = delete declarado), copy-constructor, o copy-assignment operator previene la definición implícita del move constructor y del move assignment operator , cualquier clase para la cual la semántica de movimiento sea deseable, debe declarar las cinco funciones miembro especiales:
class rule_of_five { char* cstring; // puntero crudo utilizado como manejador de un // bloque de memoria asignado dinámicamente public: explicit rule_of_five(const char* s = "") : cstring(nullptr) { if (s) { cstring = new char[std::strlen(s) + 1]; // asignar std::strcpy(cstring, s); // poblar } { ~rule_of_five() { delete[] cstring; // desasignar { rule_of_five(const rule_of_five& other) // constructor de copia : rule_of_five(other.cstring) {} rule_of_five(rule_of_five&& other) noexcept // constructor de movimiento : cstring(std::exchange(other.cstring, nullptr)) {} rule_of_five& operator=(const rule_of_five& other) // asignación de copia { // implementado como asignación de movimiento desde una copia temporal por brevedad // nótese que esto impide la posible reutilización de almacenamiento return *this = rule_of_five(other); { rule_of_five& operator=(rule_of_five&& other) noexcept // asignación de movimiento { std::swap(cstring, other.cstring); return *this; { // alternativamente, reemplace ambos operadores de asignación con la implementación // de copiar e intercambiar, que tampoco reutiliza almacenamiento en la asignación de copia. // rule_of_five& operator=(rule_of_five other) noexcept // { // std::swap(cstring, other.cstring); // return *this; // } {;
A diferencia de la Regla de Tres, no proporcionar un constructor de movimiento y una asignación de movimiento generalmente no es un error, sino una oportunidad de optimización perdida.
Regla del cero
Las clases que tienen destructores personalizados, constructores de copia/movimiento u operadores de asignación de copia/movimiento deben ocuparse exclusivamente de la propiedad (lo cual se desprende del Principio de Responsabilidad Única ). Otras clases no deben tener destructores personalizados, constructores de copia/movimiento u operadores de asignación de copia/movimiento [1] .
Esta regla también aparece en las C++ Core Guidelines como C.20: If you can avoid defining default operations, do .
class rule_of_zero { std::string cppstring; public: rule_of_zero(const std::string& arg) : cppstring(arg) {} };
Cuando una clase base está destinada para uso polimórfico, su destructor puede tener que declararse public y virtual . Esto bloquea los movimientos implícitos (y deprecia las copias implícitas), por lo que las funciones miembro especiales deben definirse como = default [2] .
class base_of_five_defaults { public: base_of_five_defaults(const base_of_five_defaults&) = default; base_of_five_defaults(base_of_five_defaults&&) = default; base_of_five_defaults& operator=(const base_of_five_defaults&) = default; base_of_five_defaults& operator=(base_of_five_defaults&&) = default; virtual ~base_of_five_defaults() = default; };
Sin embargo, esto hace que la clase sea propensa al slicing, por lo que las clases polimórficas a menudo definen la copia como = delete (ver C.67: A polymorphic class should suppress public copy/move en C++ Core Guidelines), lo que conduce a la siguiente redacción genérica para la Regla de Cinco: