informacion - La nueva palabra clave=predeterminada en C++ 11
for c++ 17 (3)
Es una cuestión de semántica en algunos casos. No es muy obvio con los constructores por defecto, pero se vuelve obvio con otras funciones miembro generadas por el compilador.
Para el constructor predeterminado, habría sido posible hacer que cualquier constructor predeterminado con un cuerpo vacío se considerara un candidato para ser un constructor trivial, lo mismo que usar =default
. Después de todo, los viejos constructores predeterminados vacíos eran legales C ++ .
struct S {
int a;
S() {} // legal C++
};
Si el compilador entiende o no que este constructor es trivial es irrelevante en la mayoría de los casos fuera de las optimizaciones (manuales o de compilación).
Sin embargo, este intento de tratar cuerpos de funciones vacías como "predeterminado" se desglosa por completo para otros tipos de funciones de miembros. Considere el constructor de copia:
struct S {
int a;
S() {}
S(const S&) {} // legal, but semantically wrong
};
En el caso anterior, el constructor de copia escrito con un cuerpo vacío ahora está equivocado . Ya no está copiando nada. Este es un conjunto de semántica muy diferente de la semántica del constructor de copia predeterminado. El comportamiento deseado requiere que escriba algún código:
struct S {
int a;
S() {}
S(const S& src) : a(src.a) {} // fixed
};
Incluso con este caso simple, sin embargo, cada vez es más una carga para el compilador verificar que el constructor de copia sea idéntico al generado por sí mismo o para que vea que el constructor de copia es trivial (equivalente a un memcpy
, básicamente). El compilador debería verificar cada expresión miembro del inicializador y asegurarse de que sea idéntica a la expresión para acceder al miembro correspondiente de la fuente y nada más, asegurarse de que ningún miembro se quede con una construcción predeterminada no trivial, etc. Está al revés en el proceso. el compilador usaría para verificar que sus propias versiones generadas de esta función son triviales.
Considere entonces el operador de asignación de copias, que puede ponerse aún más peludo, especialmente en el caso no trivial. Es una tonelada de placa de caldera que no desea tener que escribir para muchas clases, pero se verá obligado a hacerlo de todos modos en C ++ 03:
struct T {
std::shared_ptr<int> b;
T(); // the usual definitions
T(const T&);
T& operator=(const T& src) {
if (this != &src) // not actually needed for this simple example
b = src.b; // non-trivial operation
return *this;
};
Ese es un caso simple, pero ya es más código de lo que siempre quisieras forzar a escribir para un tipo tan simple como T
(especialmente una vez que lanzamos operaciones de movimiento a la mezcla). No podemos confiar en un cuerpo vacío que signifique "completar los valores predeterminados" porque el cuerpo vacío ya es perfectamente válido y tiene un significado claro. De hecho, si el cuerpo vacío se usara para denotar "completar los valores predeterminados", entonces no habría forma de hacer explícitamente un constructor de copia no operativa o similar.
De nuevo, es una cuestión de consistencia. El cuerpo vacío significa "no hacer nada", pero para cosas como los constructores de copia, realmente no quieres "no hacer nada" sino "hacer todo lo que normalmente harías si no se suprimiera". Por lo tanto =default
. Es necesario para superar las funciones suprimidas del miembro generado por el compilador, como los constructores de copiar / mover y los operadores de asignación. Entonces es simplemente "obvio" hacerlo funcionar también para el constructor predeterminado.
Hubiera sido agradable hacer que el constructor predeterminado con cuerpos vacíos y los constructores triviales de miembros / bases también se consideraran triviales del mismo modo que lo hubieran sido con =default
si solo para hacer que el código anterior sea más óptimo en algunos casos, pero la mayoría confía en el código de bajo nivel en constructores triviales por defecto para optimizaciones también se basa en constructores de copia triviales. Si vas a tener que ir y "arreglar" todos tus viejos constructores de copia, tampoco es demasiado difícil tener que arreglar todos tus viejos constructores predeterminados. También es mucho más claro y más obvio usar un valor explícito =default
para denotar tus intenciones.
Hay algunas otras cosas que las funciones de los miembros generadas por el compilador harán y también tendrías que hacer explícitamente cambios de soporte. El respaldo de constexpr
para constructores por defecto es un ejemplo. Simplemente es más fácil usar mentalmente =default
que tener que marcar las funciones con todas las otras palabras clave especiales que están implícitas en =default
y ese fue uno de los temas de C ++ 11: facilitar el lenguaje. Todavía tiene muchas verrugas y compromisos de compatibilidad con la espalda, pero está claro que es un gran avance de C ++ 03 en lo que respecta a la facilidad de uso.
No entiendo por qué alguna vez haría esto:
struct S {
int a;
S(int aa) : a(aa) {}
S() = default;
};
Por qué no solo decir:
S() {} // instead of S() = default;
¿por qué traer una nueva palabra clave para eso?
Un constructor predeterminado predeterminado se define específicamente como el mismo que un constructor predeterminado definido por el usuario sin lista de inicialización y una declaración compuesta vacía.
§12.1 / 6 [class.ctor] Un constructor predeterminado que está predeterminado y no se define como eliminado se define implícitamente cuando se usa odr para crear un objeto de su tipo de clase o cuando es explícitamente predeterminado después de su primera declaración. El constructor predeterminado implícitamente definido realiza el conjunto de inicializaciones de la clase que realizaría un constructor predeterminado escrito por el usuario para esa clase sin ctor-initializer (12.6.2) y una sentencia compuesta vacía. [...]
Sin embargo, aunque ambos constructores se comportarán igual, proporcionar una implementación vacía sí afecta algunas propiedades de la clase. Dar un constructor definido por el usuario, aunque no haga nada, hace que el tipo no sea un agregado ni trivial . Si desea que su clase sea agregada o trivial (o por transitividad, tipo POD), debe usar = default
.
§8.5.1 / 1 [dcl.init.aggr] Un agregado es una matriz o una clase sin constructores proporcionados por el usuario, [y ...]
§12.1 / 5 [class.ctor] Un constructor predeterminado es trivial si no es proporcionado por el usuario y [...]
§9 / 6 [clase] Una clase trivial es una clase que tiene un constructor predeterminado trivial y [...]
Demostrar:
#include <type_traits>
struct X {
X() = default;
};
struct Y {
Y() { };
};
int main() {
static_assert(std::is_trivial<X>::value, "X should be trivial");
static_assert(std::is_pod<X>::value, "X should be POD");
static_assert(!std::is_trivial<Y>::value, "Y should not be trivial");
static_assert(!std::is_pod<Y>::value, "Y should not be POD");
}
Además, el hecho de que un constructor esté constexpr
forma constexpr
lo hará constexpr
si el constructor implícito lo hubiera sido y también le dará la misma especificación de excepción que el constructor implícito. En el caso que ha dado, el constructor implícito no habría sido constexpr
(porque dejaría un miembro de datos sin inicializar) y también tendría una especificación de excepción vacía, por lo que no hay diferencia. Pero sí, en el caso general, puede especificar manualmente constexpr
y la especificación de excepción para que coincida con el constructor implícito.
Usar = default
cierta uniformidad, porque también se puede usar con constructores y destructores de copia / movimiento. Un constructor de copia vacía, por ejemplo, no hará lo mismo que un constructor de copia predeterminado (que realizará una copia para miembros de sus miembros). Usar la sintaxis = default
(o = delete
) de manera uniforme para cada una de estas funciones especiales para miembros hace que su código sea más fácil de leer al declarar explícitamente su intención.
n2210 proporciona algunas razones:
La gestión de los valores predeterminados tiene varios problemas:
- Las definiciones de constructores están acopladas; declarar que cualquier constructor suprime el constructor predeterminado.
- El valor predeterminado del destructor es inapropiado para las clases polimórficas, que requieren una definición explícita.
- Una vez que se suprime el valor predeterminado, no hay forma de resucitarlo.
- Las implementaciones predeterminadas a menudo son más eficientes que las implementaciones especificadas manualmente.
- Las implementaciones no predeterminadas no son triviales, lo que afecta a la semántica de tipos, por ejemplo, hace que un tipo no sea POD.
- No hay medios para prohibir una función de miembro especial o un operador global sin declarar un sustituto (no trivial).
type::type() = default; type::type() { x = 3; }
En algunos casos, el cuerpo de la clase puede cambiar sin requerir un cambio en la definición de la función miembro porque el valor predeterminado cambia con la declaración de miembros adicionales.
¿Ver Regla de los Tres se convierte en Regla de los Cinco con C ++ 11? :
Tenga en cuenta que el constructor de movimientos y el operador de asignación de movimiento no se generarán para una clase que declare explícitamente ninguna de las otras funciones especiales miembro, ese constructor de copias y el operador de asignación de copias no se generarán para una clase que explícitamente declare un movimiento o constructor operador de asignación, y que una clase con un destructor declarado explícitamente y un constructor de copia implícitamente definido o un operador de asignación de copia implícitamente definido se considera obsoleto