Namespaces
Variants

The rule of three/five/zero

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

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:

C.21: Si defines o =delete cualquier función de copia, movimiento o destructor, define o =delete todas ellas.

Enlaces externos

  1. "Regla del Cero", R. Martinho Fernandes 15/08/2012
  2. "Una Preocupación sobre la Regla del Cero", Scott Meyers, 13/03/2014 .