Funciones establecidas como valor predeterminado y eliminadas explícitamente

En C++11, las funciones establecidas como valor predeterminado y eliminadas proporcionan un control explícito sobre si las funciones miembro especiales se generan automáticamente. Las funciones eliminadas también proporcionan un lenguaje sencillo para evitar que las promociones de tipos problemáticos se produzcan en argumentos a funciones de todos los tipos (funciones miembro especiales y funciones miembro normales y funciones no miembro), lo que de lo contrario provocaría una llamada a función no deseada.

Ventajas de las funciones establecidas como valor predeterminado o eliminadas explícitamente

En C++, el compilador genera automáticamente el constructor predeterminado, el constructor de copia, el operador de asignación de copia y el destructor de un tipo si no declara su propio. Estas funciones se conocen como funciones miembro especiales y son lo que hacen que los tipos sencillos definidos por el usuario en C++ se comporten como las estructuras en C. Es decir, puede crear, copiar y destruirlos sin esfuerzo de codificación adicional. C++11 aporta semántica de movimiento al lenguaje y agrega el constructor de movimiento y el operador de asignación de movimiento a la lista de funciones miembro especiales que el compilador puede generar automáticamente.

Esto es útil en el caso de tipos simples, pero los tipos complejos suelen definir una o varias funciones miembro especiales por sí mismos, lo que puede impedir la generación automática de otras funciones miembro especiales. En la práctica:

  • Si se declara explícitamente un constructor, no se genera automáticamente ningún constructor predeterminado.

  • Si se declara explícitamente un destructor virtual, no se genera automáticamente ningún destructor predeterminado.

  • Si se declara explícitamente un constructor de movimiento o un operador de asignación de movimiento, entonces:

    • No se genera automáticamente ningún constructor de copia.

    • No se genera automáticamente ningún operador de asignación de copia.

  • Si se declara explícitamente un constructor de copia, un operador de asignación de copia, un constructor de movimiento, un operador de asignación de movimiento o un destructor, entonces:

    • No se genera automáticamente ningún constructor de movimiento.

    • No se genera automáticamente ningún operador de asignación de movimiento.

Nota:

Además, el estándar C++11 especifica las reglas adicionales siguientes:

  • Si se declara explícitamente un constructor de copia o un destructor, la generación automática del operador de asignación de copia está desusada.
  • Si se declara explícitamente un operador de asignación de copia o un destructor, la generación automática del constructor de copia está en desuso.

En ambos casos, Visual Studio continúa generando automáticamente las funciones necesarias implícitamente y no emite una advertencia de forma predeterminada. Desde la versión 17.7 de Visual Studio 2022, C5267 se puede habilitar para emitir una advertencia.

Las consecuencias de estas reglas también pueden propagarse a las jerarquías de objetos. Por ejemplo, si por algún motivo una clase base no puede tener un constructor predeterminado al que se pueda llamar desde una clase derivada (es decir, un public constructor o protected que no tome parámetros), una clase que deriva de ella no puede generar automáticamente su propio constructor predeterminado.

Estas reglas pueden complicar la implementación de lo que debe ser tipos directos, definidos por el usuario y expresiones comunes de C++, por ejemplo, haciendo que un tipo definido por el usuario no se pueda copiar declarando el constructor de copia y el operador de asignación de copia de forma privada y no definiéndolas.

struct noncopyable
{
  noncopyable() {};

private:
  noncopyable(const noncopyable&);
  noncopyable& operator=(const noncopyable&);
};

Antes de C++11, este fragmento de código era la forma idiomática de tipos no copiables. Sin embargo, plantea varios problemas:

  • El constructor de copia debe declararse de forma privada para ocultarlo, pero como se ha declarado plenamente, se impide la generación automática del constructor predeterminado. Tiene que definir explícitamente el constructor predeterminado si desea uno, aunque no haga nada.

  • Incluso si el constructor predeterminado definido explícitamente no hace nada, el compilador considera que no estrivial. Es menos eficaz que un constructor predeterminado generado automáticamente e impide que noncopyable sea un verdadero tipo POD.

  • Aunque el constructor de copia y el operador de asignación de copia estén ocultos para el código externo, las funciones miembro y los elementos friend de noncopyable aún pueden verlos y llamarlos. Si se declaran pero no se definen, llamarlos produce un error del enlazador.

  • Aunque se trata de una expresión comúnmente aceptada, la intención no es clara a menos que comprenda todas las reglas para la generación automática de las funciones miembro especiales.

En C++11, la expresión no copiable se puede implementar de forma más sencilla.

struct noncopyable
{
  noncopyable() =default;
  noncopyable(const noncopyable&) =delete;
  noncopyable& operator=(const noncopyable&) =delete;
};

Observe cómo se resuelven los problemas con la expresión anterior a C++11:

  • La generación del constructor predeterminado todavía se puede evitar declarando el constructor de copia, pero se puede volver a utilizar si se establece explícitamente como valor predeterminado.

  • Las funciones miembro especiales predeterminadas explícitamente siguen siendo triviales, por lo que no hay ninguna penalización de rendimiento y noncopyable no se impide que sea un tipo POD verdadero.

  • El constructor de copia y el operador de asignación de copia son públicos pero se han eliminado. Se trata de un error en tiempo de compilación para definir o llamar a una función eliminada.

  • La intención queda clara para cualquiera que entienda =default y =delete. No es necesario comprender las reglas de generación automática de funciones miembro especiales.

Existen expresiones similares para crear tipos definidos por el usuario que no son extraíbles, que solo se pueden asignar dinámicamente o que no se pueden asignar dinámicamente. Cada una de estas expresiones tiene implementaciones previas a C++11 que experimentan problemas similares, y que se resuelven de manera similar en C++11 mediante su implementación basada en funciones miembro especiales como valores predeterminados y eliminadas.

Funciones establecidas como valor predeterminado explícitamente

Puede establecer de forma predeterminada cualquiera de las funciones miembro especiales, para indicar explícitamente que la función miembro especial usa la implementación predeterminada, definir la función miembro especial con un calificador de acceso no públicos o restablecer una función miembro especial cuya generación automática fue impedida por otras circunstancias.

Una función miembro especial se establece como predeterminada declarándola como en este ejemplo:

struct widget
{
  widget()=default;

  inline widget& operator=(const widget&);
};

inline widget& widget::operator=(const widget&) =default;

Tenga en cuenta que puede establecer como valor predeterminado una función miembro especial fuera del cuerpo de una clase siempre y cuando se pueda insertar.

Debido a las ventajas de rendimiento que ofrecen las funciones miembro especiales triviales, se recomienda elegir funciones miembro especiales generadas automáticamente en lugar de cuerpos de función vacíos cuando se desee el comportamiento predeterminado. Se puede hacer si se establece explícitamente como valor predeterminado la función miembro especial o si no la declara (y tampoco declara otras funciones miembro especiales que impedirían que se generara automáticamente).

Funciones eliminadas

Puede eliminar funciones miembro especiales y funciones miembro normales y funciones no miembro para evitar que se definan o llamen a ellas. La eliminación de funciones miembro especiales proporciona una forma más limpia de impedir que el compilador genere funciones miembro especiales que no se desean. La función debe eliminarse a medida que se declara; no se puede eliminar después de la forma en que se puede declarar una función y, después, se puede establecer el valor predeterminado posterior.

struct widget
{
  // deleted operator new prevents widget from being dynamically allocated.
  void* operator new(std::size_t) = delete;
};

La eliminación de funciones miembro normales o funciones que no son miembros impide que se llame a una función no deseada. Esto funciona porque las funciones eliminadas siguen participando en la resolución de sobrecargas y proporcionan una mejor coincidencia que la función a la que se puede llamar después de que se promuevan los tipos. La llamada a función se resuelve en la función más específica, pero eliminada, y produce un error del compilador.

// deleted overload prevents call through type promotion of float to double from succeeding.
void call_with_true_double_only(float) =delete;
void call_with_true_double_only(double param) { return; }

Observe en el ejemplo anterior que llamar a call_with_true_double_only mediante un float argumento provocaría un error del compilador, pero llamar al call_with_true_double_only uso de un int argumento no; en el int caso, el argumento se promoverá de int a y llamará correctamente a double la double versión de la función, aunque eso podría no ser lo que pretende. Para asegurarse de que cualquier llamada a esta función mediante un argumento no doble provoca un error del compilador, puede declarar una versión de plantilla de la función eliminada.

template < typename T >
void call_with_true_double_only(T) =delete; //prevent call through type promotion of any T to double from succeeding.

void call_with_true_double_only(double param) { return; } // also define for const double, double&, etc. as needed.