No entiendo por qué 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 sintaxis para eso?
No entiendo por qué 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 sintaxis para eso?
Respuestas:
Un constructor predeterminado predeterminado se define específicamente como el mismo que un constructor predeterminado definido por el usuario sin una 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 se omite explícitamente después de su primera declaración. El constructor predeterminado definido implícitamente 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 declaración compuesta vacía. [...]
Sin embargo, si bien ambos constructores se comportarán igual, proporcionar una implementación vacía 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 y tampoco trivial . Si desea que su clase sea un tipo agregado o trivial (o por transitividad, un tipo POD), entonces debe usarlo = 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 trivial predeterminado 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, la omisión explícita de un constructor lo hará constexpr
si el constructor implícito hubiera sido y también le dará la misma especificación de excepción que el constructor implícito habría tenido. En el caso que ha proporcionado, el constructor implícito no lo 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, podría especificar manualmente constexpr
y la especificación de excepción para que coincida con el constructor implícito.
El uso = default
aporta cierta uniformidad, porque también se puede usar con constructores y destructores de copia / movimiento. Un constructor de copia vacío, por ejemplo, no hará lo mismo que un constructor de copia predeterminado (que realizará una copia de sus miembros). El uso uniforme de la sintaxis = default
(o = delete
) para cada una de estas funciones miembro especiales hace que su código sea más fácil de leer al indicar explícitamente su intención.
constexpr
constructor (7.1.5), el constructor predeterminado definido implícitamente es constexpr
".
constexpr
si la declaración implícita sería, (b) se considera implícitamente que tiene la misma especificación de excepción como si se hubiera declarado implícitamente (15.4), ... "No hace ninguna diferencia en este caso específico, pero en general foo() = default;
tiene una ligera ventaja sobre foo() {}
.
constexpr
(ya que un miembro de datos no se inicializa) y su especificación de excepción permite todas las excepciones. Lo aclararé más.
constexpr
(lo que mencionaste no debería hacer una diferencia aquí): struct S1 { int m; S1() {} S1(int m) : m(m) {} }; struct S2 { int m; S2() = default; S2(int m) : m(m) {} }; constexpr S1 s1 {}; constexpr S2 s2 {};
solo s1
da un error, no s2
. Tanto en clang como en g ++.
Tengo un ejemplo que mostrará la diferencia:
#include <iostream>
using namespace std;
class A
{
public:
int x;
A(){}
};
class B
{
public:
int x;
B()=default;
};
int main()
{
int x = 5;
new(&x)A(); // Call for empty constructor, which does nothing
cout << x << endl;
new(&x)B; // Call for default constructor
cout << x << endl;
new(&x)B(); // Call for default constructor + Value initialization
cout << x << endl;
return 0;
}
Salida:
5
5
0
Como podemos ver, la llamada al constructor A () vacío no inicializa los miembros, mientras que B () lo hace.
n2210 proporciona algunas razones:
La gestión de los valores predeterminados tiene varios problemas:
- Las definiciones de constructor están acopladas; declarar cualquier constructor suprime el constructor predeterminado.
- El valor predeterminado del destructor es inapropiado para las clases polimórficas, ya que requiere una definición explícita.
- Una vez que se suprime un valor predeterminado, no hay forma de resucitarlo.
- Las implementaciones predeterminadas son a menudo más eficientes que las implementaciones especificadas manualmente.
- Las implementaciones no predeterminadas no son triviales, lo que afecta la semántica de tipos, por ejemplo, hace que un tipo no sea POD.
- No hay medios para prohibir una función 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 de miembro porque el valor predeterminado cambia con la declaración de miembros adicionales.
¿Ver Regla de tres se convierte en Regla de cinco con C ++ 11? :
Tenga en cuenta que el constructor de movimientos y el operador de asignación de movimientos no se generarán para una clase que declare explícitamente ninguna de las otras funciones miembro especiales, ese constructor de copia y el operador de asignación de copias no se generarán para una clase que declare explícitamente un constructor o movimiento de movimiento operador de asignación, y que una clase con un destructor declarado explícitamente y un constructor de copia definido implícitamente o un operador de asignación de copia definido implícitamente se considera obsoleto
= default
en general, en lugar de razones para hacer = default
en un constructor frente a hacer { }
.
{}
era ya una característica de la lengua antes de la introducción de =default
estas razones no implícitamente se basan en la distinción (por ejemplo, "no hay medios para resucitar [un defecto suprimido]" implica que {}
es no equivalente al valor predeterminado )
Es una cuestión de semántica en algunos casos. No es muy obvio con los constructores predeterminados, 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 candidato para ser un constructor trivial, igual que usar =default
. Después de todo, los viejos constructores predeterminados vacíos eran C ++ legales .
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 compiladoras).
Sin embargo, este intento de tratar los cuerpos de funciones vacíos como "predeterminados" se desglosa por completo para otros tipos de funciones miembro. 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 a la semántica del constructor de copia predeterminado. El comportamiento deseado requiere que escriba un código:
struct S {
int a;
S() {}
S(const S& src) : a(src.a) {} // fixed
};
Incluso con este caso simple, sin embargo, se está volviendo una carga mucho más pesada para el compilador verificar que el constructor de la copia sea idéntico al que generaría o para ver que el constructor de la copia es trivial (equivalente a memcpy
, básicamente, a ) El compilador tendría que verificar la expresión de inicialización de cada miembro 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 no queden miembros con una construcción predeterminada no trivial, etc. Está al revés en una forma del proceso el compilador lo usaría para verificar que sus propias versiones generadas de esta función sean triviales.
Considere entonces el operador de asignación de copias que puede ser aún más complicado, especialmente en el caso no trivial. Es una tonelada de placa de caldera que no desea tener que escribir para muchas clases, pero se ve 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 del que te gustaría que te obligaran a escribir para un tipo tan simple como T
(especialmente una vez que arrojamos las operaciones de movimiento a la mezcla). No podemos confiar en un cuerpo vacío que significa "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.
Nuevamente es una cuestión de consistencia. El cuerpo vacío significa "no hacer nada", pero para cosas como los constructores de copias realmente no quieres "no hacer nada", sino "hacer todas las cosas que normalmente harías si no se suprimieran". Por lo tanto =default
. Es necesario para superar las funciones miembro suprimidas generadas por el compilador, como copiar / mover constructores y operadores de asignación. Entonces es "obvio" hacer que funcione también para el constructor predeterminado.
Podría haber sido agradable hacer que el constructor predeterminado con cuerpos vacíos y los constructores de miembros / bases triviales también se consideren triviales tal como lo hubieran sido =default
si solo hubiera sido para optimizar el código antiguo en algunos casos, pero la mayoría de los códigos de bajo nivel que se basan en trivial Los constructores predeterminados para optimizaciones también se basan en constructores de copia triviales. Si va a tener que ir y "arreglar" todos sus viejos constructores de copias, tampoco es realmente difícil tener que arreglar todos sus viejos constructores predeterminados. También es mucho más claro y más obvio usar un explícito =default
para denotar tus intenciones.
Hay algunas otras cosas que harán las funciones miembro generadas por el compilador que también tendrías que hacer cambios explícitos para soportar. El soporte constexpr
para constructores predeterminados es un ejemplo. Es más fácil de usar mentalmente =default
que tener que marcar funciones con todas las otras palabras clave especiales y las que están implícitas =default
y ese fue uno de los temas de C ++ 11: facilitar el lenguaje. Todavía tiene muchas verrugas y compromisos de compatibilidad inversa, pero está claro que es un gran paso adelante de C ++ 03 en lo que respecta a la facilidad de uso.
= default
lo hiciera a=0;
y no fue así! Tuve que dejarlo a favor de : a(0)
. Todavía estoy confundido acerca de lo útil que = default
es, ¿se trata de rendimiento? ¿se romperá en alguna parte si simplemente no lo uso = default
? Traté de leer todas las respuestas aquí. Compre. Soy nuevo en algunas cosas de C ++ y tengo muchos problemas para entenderlo.
a=0
ejemplo se debe al comportamiento de los tipos triviales, que son un tema separado (aunque relacionado).
= default
y todavía subvención a
será =0
? ¿de alguna manera? ¿cree que podría crear una nueva pregunta como "¿cómo tener un constructor = default
y otorgan los campos se inician correctamente?", por cierto tuve el problema de una struct
y no una class
, y la aplicación se está ejecutando correctamente incluso sin usar = default
, que pueda agregue una estructura mínima en esa pregunta si es buena :)
struct { int a = 0; };
si luego decide que necesita un constructor, puede usarlo por defecto, pero tenga en cuenta que el tipo no será trivial (lo cual está bien).
Debido a la depreciación std::is_pod
y su alternativa std::is_trivial && std::is_standard_layout
, el fragmento de la respuesta de @JosephMansfield se convierte en:
#include <type_traits>
struct X {
X() = default;
};
struct Y {
Y() {}
};
int main() {
static_assert(std::is_trivial_v<X>, "X should be trivial");
static_assert(std::is_standard_layout_v<X>, "X should be standard layout");
static_assert(!std::is_trivial_v<Y>, "Y should not be trivial");
static_assert(std::is_standard_layout_v<Y>, "Y should be standard layout");
}
Tenga en cuenta que Y
todavía tiene un diseño estándar.
default
no es una nueva palabra clave, es simplemente un nuevo uso de una palabra clave ya reservada.