¿En qué se diferencia “= default” de “{}” para el constructor y destructor por defecto?

169

Originalmente publiqué esto como una pregunta solo sobre destructores, pero ahora estoy agregando la consideración del constructor predeterminado. Aquí está la pregunta original:

Si quiero darle a mi clase un destructor que sea virtual, pero que por lo demás sea el mismo que generaría el compilador, puedo usar =default:

class Widget {
public:
   virtual ~Widget() = default;
};

Pero parece que puedo obtener el mismo efecto con menos tipeo usando una definición vacía:

class Widget {
public:
   virtual ~Widget() {}
};

¿Hay alguna forma en que estas dos definiciones se comporten de manera diferente?

Según las respuestas publicadas para esta pregunta, la situación para el constructor predeterminado parece similar. Dado que casi no hay diferencia en el significado entre " =default" y " {}" para los destructores, ¿hay una diferencia similar en el significado entre estas opciones para los constructores predeterminados? Es decir, suponiendo que quiero crear un tipo donde los objetos de ese tipo serán creados y destruidos, ¿por qué querría decir

Widget() = default;

en vez de

Widget() {}

?

Pido disculpas si extender esta pregunta después de su publicación original está violando algunas reglas SO. Publicar una pregunta casi idéntica para los constructores predeterminados me pareció la opción menos deseable.

KnowItAllWannabe
fuente
1
No es que yo sepa, pero = defaultes más explícito en la OMI, y es coherente con el soporte con constructores.
Chris
11
No estoy seguro, pero creo que el primero se ajusta a la definición de "destructor trivial", mientras que el segundo no. Así std::has_trivial_destructor<Widget>::valuees truepara el primero, pero falsepara el segundo. Tampoco sé cuáles son las implicaciones de eso. :)
GManNickG
10
Un destructor virtual nunca es trivial.
Luc Danton
@LucDanton: ¡Supongo que abrir los ojos y mirar el código también funcionaría! Gracias por corregir
GManNickG
Relacionado: stackoverflow.com/questions/20828907/…
Gabriel Staples

Respuestas:

103

Esta es una pregunta completamente diferente cuando se pregunta sobre constructores que sobre destructores.

Si su destructor es virtual, entonces la diferencia es insignificante, como señaló Howard . Sin embargo, si su destructor no era virtual , es una historia completamente diferente. Lo mismo es cierto para los constructores.

El uso de la = defaultsintaxis para funciones miembro especiales (constructor predeterminado, copiar / mover constructores / asignación, destructores, etc.) significa algo muy diferente de simplemente hacer {}. Con este último, la función se convierte en "proporcionada por el usuario". Y eso lo cambia todo.

Esta es una clase trivial según la definición de C ++ 11:

struct Trivial
{
  int foo;
};

Si intentas construir uno por defecto, el compilador generará un constructor por defecto automáticamente. Lo mismo ocurre con la copia / movimiento y la destrucción. Debido a que el usuario no proporcionó ninguna de estas funciones miembro, la especificación C ++ 11 lo considera una clase "trivial". Por lo tanto, es legal hacer esto, como recordar sus contenidos para inicializarlos, etc.

Esta:

struct NotTrivial
{
  int foo;

  NotTrivial() {}
};

Como su nombre indica, esto ya no es trivial. Tiene un constructor predeterminado proporcionado por el usuario. No importa si está vacío; En lo que respecta a las reglas de C ++ 11, este no puede ser un tipo trivial.

Esta:

struct Trivial2
{
  int foo;

  Trivial2() = default;
};

Nuevamente, como su nombre lo indica, este es un tipo trivial. ¿Por qué? Porque le dijiste al compilador que generara automáticamente el constructor predeterminado. Por lo tanto, el constructor no es "proporcionado por el usuario". Y, por lo tanto, el tipo cuenta como trivial, ya que no tiene un constructor predeterminado proporcionado por el usuario.

La = defaultsintaxis está principalmente allí para hacer cosas como copiar constructores / asignaciones, cuando agrega funciones miembro que impiden la creación de tales funciones. Pero también desencadena un comportamiento especial del compilador, por lo que también es útil en constructores / destructores predeterminados.

Nicol Bolas
fuente
2
Entonces, el problema clave parece ser si la clase resultante es trivial, y subyacente a ese problema está la diferencia entre una función especial declarada por el usuario (que es el caso de las =defaultfunciones) y las funciones proporcionadas por el usuario (que es el caso de {}). Tanto las funciones declaradas por el usuario como las proporcionadas por el usuario pueden evitar la generación de otra función miembro especial (por ejemplo, un destructor declarado por el usuario impide la generación de las operaciones de movimiento), pero solo una función especial proporcionada por el usuario hace que una clase no sea trivial. ¿Correcto?
KnowItAllWannabe
@KnowItAllWannabe: Esa es la idea general, sí.
Nicol Bolas
Estoy eligiendo esto como la respuesta aceptada, solo porque cubre tanto a los constructores como a los destructores (por referencia a la respuesta de Howard).
KnowItAllWannabe
Parece ser una palabra que falta aquí "en lo que respecta a las reglas de C ++ 11, los derechos de un tipo trivial" Lo arreglaría, pero no estoy absolutamente seguro al 100% de lo que se pretendía.
jcoder
2
= defaultparece ser útil para forzar al compilador a generar un constructor predeterminado a pesar de la presencia de otros constructores; el constructor predeterminado no se declara implícitamente si se proporcionan otros constructores declarados por el usuario.
bgfvdu3w
42

Ambos son no triviales.

Ambos tienen la misma especificación sin excepción, dependiendo de la especificación sin excepción de las bases y los miembros.

La única diferencia que estoy detectando hasta ahora es que si Widgetcontiene una base o miembro con un destructor inaccesible o eliminado:

struct A
{
private:
    ~A();
};

class Widget {
    A a_;
public:
#if 1
   virtual ~Widget() = default;
#else
   virtual ~Widget() {}
#endif
};

Entonces la =defaultsolución se compilará, pero Widgetno será un tipo destructible. Es decir, si intenta destruir un Widget, obtendrá un error en tiempo de compilación. Pero si no lo hace, tiene un programa que funciona.

Otoh, si proporciona el destructor proporcionado por el usuario , las cosas no se compilarán si destruye o no Widget:

test.cpp:8:7: error: field of type 'A' has private destructor
    A a_;
      ^
test.cpp:4:5: note: declared private here
    ~A();
    ^
1 error generated.
Howard Hinnant
fuente
9
Interesante: en otras palabras, con =default;el compilador no generará el destructor a menos que se use, y por lo tanto no desencadenará un error. Esto me parece extraño, aunque no sea necesariamente un error. No puedo imaginar que este comportamiento sea obligatorio en el estándar.
Nik Bougalis
"Entonces se compilará la solución = predeterminada" No, no lo hará. Sólo probado en vs
nano
¿Cuál fue el mensaje de error y qué versión de VS?
Howard Hinnant el
35

La diferencia importante entre

class B {
    public:
    B(){}
    int i;
    int j;
};

y

class B {
    public:
    B() = default;
    int i;
    int j;
};

es que el constructor predeterminado definido con B() = default;se considera no definido por el usuario . Esto significa que en caso de inicialización de valor como en

B* pb = new B();  // use of () triggers value-initialization

Se llevará a cabo un tipo especial de inicialización que no utiliza un constructor en absoluto y para los tipos integrados esto dará como resultado una inicialización cero . En caso de que B(){}esto no ocurra. El estándar C ++ n3337 § 8.5 / 7 dice

Para inicializar un objeto de tipo T significa:

- si T es un tipo de clase (posiblemente calificado por cv) (Cláusula 9) con un constructor proporcionado por el usuario (12.1), se llama al constructor predeterminado para T (y la inicialización está mal formada si T no tiene un constructor predeterminado accesible );

- si T es un tipo de clase no sindicalizado (posiblemente calificado por cv) sin un constructor proporcionado por el usuario , entonces el objeto se inicializa a cero y, si el constructor predeterminado declarado implícitamente por T no es trivial, se llama a ese constructor.

- si T es un tipo de matriz, entonces cada elemento tiene un valor inicializado; - de lo contrario, el objeto está inicializado en cero.

Por ejemplo:

#include <iostream>

class A {
    public:
    A(){}
    int i;
    int j;
};

class B {
    public:
    B() = default;
    int i;
    int j;
};

int main()
{
    for( int i = 0; i < 100; ++i) {
        A* pa = new A();
        B* pb = new B();
        std::cout << pa->i << "," << pa->j << std::endl;
        std::cout << pb->i << "," << pb->j << std::endl;
        delete pa;
        delete pb;
    }
  return 0;
}

posible resultado:

0,0
0,0
145084416,0
0,0
145084432,0
0,0
145084416,0
//...

http://ideone.com/k8mBrd

4pie0
fuente
Entonces, ¿por qué "{}" y "= default" siempre inicializan un std :: string ideone.com/LMv5Uf ?
nawfel bgh 01 de
1
@nawfelbgh El constructor predeterminado A () {} llama al constructor predeterminado para std :: string ya que este es un tipo que no es POD. El ctor predeterminado de std :: string lo inicializa en una cadena vacía de tamaño 0. El ctor predeterminado para escalares no hace nada: los objetos con duración de almacenamiento automática (y sus subobjetos) se inicializan en valores indeterminados.
4pie0