Qual é a diferença entre "= default" e "{}" para o construtor e destruidor padrão?


169

Originalmente, eu postei isso como uma pergunta apenas sobre destruidores, mas agora estou adicionando consideração ao construtor padrão. Aqui está a pergunta original:

Se eu quiser dar à minha classe um destruidor que seja virtual, mas que seja igual ao que o compilador geraria, eu posso usar =default:

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

Mas parece que posso obter o mesmo efeito com menos digitação usando uma definição vazia:

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

Existe alguma maneira pela qual essas duas definições se comportam de maneira diferente?

Com base nas respostas postadas para esta pergunta, a situação do construtor padrão parece semelhante. Dado que quase não há diferença de significado entre " =default" e " {}" para destruidores, da mesma forma quase não há diferença de significado entre essas opções para construtores padrão? Ou seja, supondo que eu queira criar um tipo em que os objetos desse tipo sejam criados e destruídos, por que eu gostaria de dizer

Widget() = default;

ao invés de

Widget() {}

?

Peço desculpas se estender esta pergunta após a postagem original violar algumas regras de SO. A publicação de uma pergunta quase idêntica para os construtores padrão me pareceu a opção menos desejável.


1
Não que eu saiba, mas = defaulté uma imo mais explícita e é consistente com o suporte a ela com construtores.
Chris27

11
Não sei ao certo, mas acho que o primeiro está de acordo com a definição de "destruidor trivial", enquanto o segundo não. Assim std::has_trivial_destructor<Widget>::valueé truepara o primeiro, mas falsepara o segundo. Quais são as implicações disso também não sei. :)
GManNickG

10
Um destruidor virtual nunca é trivial.
Luc Danton

@ LucDanton: Suponho que abrir meus olhos e olhar o código funcionaria também! Obrigado por corrigir.
GManNickG

Respostas:


103

Essa é uma pergunta completamente diferente quando se pergunta sobre construtores e destruidores.

Se o seu destruidor é virtual, então a diferença é insignificante, como Howard apontou . No entanto, se o seu destruidor não era virtual , é uma história completamente diferente. O mesmo se aplica aos construtores.

Usar a = defaultsintaxe para funções-membro especiais (construtor padrão, copiar / mover construtores / atribuição, destruidores etc.) significa algo muito diferente de simplesmente fazer {}. Com o último, a função se torna "fornecida pelo usuário". E isso muda tudo.

Esta é uma classe trivial pela definição do C ++ 11:

struct Trivial
{
  int foo;
};

Se você tentar construir um padrão, o compilador gerará um construtor padrão automaticamente. O mesmo vale para cópia / movimento e destruição. Como o usuário não forneceu nenhuma dessas funções de membro, a especificação do C ++ 11 considera isso uma classe "trivial". Portanto, é legal fazer isso, como copiar o conteúdo para inicializá-lo e assim por diante.

Este:

struct NotTrivial
{
  int foo;

  NotTrivial() {}
};

Como o nome sugere, isso não é mais trivial. Ele possui um construtor padrão fornecido pelo usuário. Não importa se está vazio; No que diz respeito às regras do C ++ 11, esse não pode ser um tipo trivial.

Este:

struct Trivial2
{
  int foo;

  Trivial2() = default;
};

Novamente, como o nome sugere, esse é um tipo trivial. Por quê? Porque você disse ao compilador para gerar automaticamente o construtor padrão. O construtor, portanto, não é "fornecido pelo usuário". E, portanto, o tipo conta como trivial, pois não possui um construtor padrão fornecido pelo usuário.

A = defaultsintaxe existe principalmente para fazer coisas como construtores de cópia / atribuição, quando você adiciona funções membro que impedem a criação de tais funções. Mas também aciona um comportamento especial do compilador, por isso também é útil em construtores / destruidores padrão.


2
Portanto, a questão principal parece ser se a classe resultante é trivial, e subjacente a essa questão está a diferença entre uma função especial sendo declarada pelo usuário (que é o caso das =defaultfunções) e fornecidas pelo usuário (que é o caso {}). As funções declaradas pelo usuário e fornecidas pelo usuário podem impedir a geração de outra função membro especial (por exemplo, um destruidor declarado pelo usuário impede a geração das operações de movimentação), mas apenas uma função especial fornecida pelo usuário torna uma classe não trivial. Certo?
KnowItAllWannabe

@KnowItAllWannabe: Essa é a ideia geral, sim.
Nicol Bolas

Estou escolhendo esta como a resposta aceita, apenas porque abrange os construtores e (por referência à resposta de Howard) os destruidores.
KnowItAllWannabe

Parece ser uma palavra que falta aqui "no que diz respeito às regras do C ++ 11, você tem os direitos de um tipo trivial" Eu consertaria, mas não tenho 100% de certeza do que se pretendia.
jcoder

2
= defaultparece ser útil para forçar o compilador a gerar um construtor padrão, apesar da presença de outros construtores; o construtor padrão não é declarado implicitamente se forem fornecidos outros construtores declarados pelo usuário.
precisa saber é o seguinte

42

Ambos são não triviais.

Ambos têm a mesma especificação noexcept, dependendo da especificação noexcept das bases e membros.

A única diferença que estou detectando até agora é que, se Widgetcontiver uma base ou membro com um destruidor inacessível ou excluído:

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

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

Em seguida, a =defaultsolução será compilada, mas Widgetnão será do tipo destrutível. Ou seja, se você tentar destruir a Widget, receberá um erro em tempo de compilação. Mas se não, você tem um programa de trabalho.

Otoh, se você fornecer o destruidor fornecido pelo usuário , as coisas não serão compiladas, independentemente de você destruir ou não um 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.

9
Interessante: em outras palavras, =default;o compilador não gera o destruidor a menos que seja usado e, portanto, não gera um erro. Isso me parece estranho, mesmo que não seja necessariamente um bug. Não consigo imaginar que esse comportamento seja obrigatório no padrão.
Nik Bougalis

"Então a solução = default será compilada" Não, não será. Apenas testado em vs.
nano

Qual foi a mensagem de erro e qual versão do VS?
Howard Hinnant

35

A diferença importante entre

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

e

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

é que o construtor padrão definido com B() = default;é considerado não definido pelo usuário . Isso significa que, no caso de inicialização de valor como em

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

ocorrerá um tipo especial de inicialização que não usará nenhum construtor e, para tipos internos, isso resultará em inicialização zero . Caso B(){}isso não ocorra. O Padrão C ++ n3337 § 8.5 / 7 diz

Valorizar a inicialização de um objeto do tipo T significa:

- se T for um tipo de classe (possivelmente qualificado para cv) (Cláusula 9) com um construtor fornecido pelo usuário (12.1), o construtor padrão para T será chamado (e a inicialização será mal formada se T não tiver um construtor padrão acessível) );

- se T é um tipo de classe de não união (possivelmente qualificado para cv) sem um construtor fornecido pelo usuário , o objeto é inicializado com zero e, se o construtor padrão declarado implicitamente por T não é trivial, esse construtor é chamado.

- se T é um tipo de matriz, então cada elemento é inicializado por valor; - caso contrário, o objeto será inicializado com zero.

Por exemplo:

#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;
}

resultado possível:

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

http://ideone.com/k8mBrd


Então, por que "{}" e "= default" sempre inicializam um std :: string ideone.com/LMv5Uf ?
Nawfel bgh

1
@nawfelbgh O construtor padrão A () {} chama o construtor padrão para std :: string, pois esse é o tipo não-POD. O ctor padrão de std :: string inicializa para uma string vazia de tamanho 0. O controlador padrão para escalares não faz nada: os objetos com duração automática de armazenamento (e seus subobjetos) são inicializados com valores indeterminados.
4pie0
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.