La nueva sintaxis "= predeterminado" en C ++ 11


136

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?


30
Nitpick: defaultno es una nueva palabra clave, es simplemente un nuevo uso de una palabra clave ya reservada.


Mey be Esta pregunta podría ayudarte.
FreeNickname

77
Además de las otras respuestas, también argumentaría que '= default;' es más autodocumentado
Mark

Respuestas:


136

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á constexprsi 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 constexpry la especificación de excepción para que coincida con el constructor implícito.

El uso = defaultaporta 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.


Casi. 12.1 / 6: "Si ese constructor predeterminado escrito por el usuario satisfaría los requisitos de un constexprconstructor (7.1.5), el constructor predeterminado definido implícitamente es constexpr".
Casey

En realidad, 8.4.2 / 2 es más informativo: "Si una función se omite explícitamente en su primera declaración, (a) se considera implícitamente constexprsi 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() {}.
Casey

2
¿Dices que no hay diferencia y luego explicas las diferencias?

@hvd En este caso no hay diferencia, porque la declaración implícita no lo sería 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.
Joseph Mansfield

2
Gracias por la aclaración. Sin embargo, todavía parece haber una diferencia con 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 s1da un error, no s2. Tanto en clang como en g ++.

10

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.


77
¿podría explicar esta sintaxis -> new (& x) A ();
Vencat

55
Estamos creando un nuevo objeto en la memoria iniciada desde la dirección de la variable x (en lugar de una nueva asignación de memoria). Esta sintaxis se usa para crear objetos en la memoria preasignada. Como en nuestro caso el tamaño de B = el tamaño de int, entonces new (& x) A () creará un nuevo objeto en lugar de x variable.
Slavenskij

Gracias por tu explicación.
Vencat

1
Obtengo resultados diferentes con gcc 8.3: ideone.com/XouXux
Adam.Er8

Incluso con C ++ 14, obtengo
Bhushan

9

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


1
Son razones para tener = defaulten general, en lugar de razones para hacer = defaulten un constructor frente a hacer { }.
Joseph Mansfield

@JosephMansfield Es cierto, pero ya que {}era ya una característica de la lengua antes de la introducción de =defaultestas 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 )
Kyle Strand

7

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 =defaultsi 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 =defaultpara 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 constexprpara constructores predeterminados es un ejemplo. Es más fácil de usar mentalmente =defaultque tener que marcar funciones con todas las otras palabras clave especiales y las que están implícitas =defaulty 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.


¡Tenía un problema donde esperaba que = defaultlo hiciera a=0;y no fue así! Tuve que dejarlo a favor de : a(0). Todavía estoy confundido acerca de lo útil que = defaultes, ¿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.
Acuario Power

@AquariusPower: no se trata solo de rendimiento, sino que también se requiere en algunos casos en torno a excepciones y otras semánticas. A saber, un operador predeterminado puede ser trivial, pero un operador no predeterminado nunca puede ser trivial, y algunos códigos usarán técnicas de metaprogramación para alterar el comportamiento o incluso prohibir tipos con operaciones no triviales. Su a=0ejemplo se debe al comportamiento de los tipos triviales, que son un tema separado (aunque relacionado).
Sean Middleditch

lo hace significa que es posible tener = defaulty todavía subvención aserá =0? ¿de alguna manera? ¿cree que podría crear una nueva pregunta como "¿cómo tener un constructor = defaulty otorgan los campos se inician correctamente?", por cierto tuve el problema de una structy 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 :)
Aquarius Power

1
@AquariusPower: puede usar inicializadores de miembros de datos no estáticos. Escriba su estructura de la siguiente manera: 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).
Sean Middleditch

2

Debido a la depreciación std::is_pody 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 Ytodavía tiene un diseño estándar.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.