A nova sintaxe “= padrão” em C ++ 11


136

Eu não entendo por que eu faria isso:

struct S { 
    int a; 
    S(int aa) : a(aa) {} 
    S() = default; 
};

Por que não dizer:

S() {} // instead of S() = default;

por que trazer uma nova sintaxe para isso?


30
Nitpick: defaultnão é uma nova palavra-chave, é apenas um novo uso de uma palavra-chave já reservada.


Mey ser Esta questão pode ajudá-lo.
FreeNickname

7
Além das outras respostas, eu também argumentaria que '= default;' é mais auto-documentado.
Mark

Respostas:


136

Um construtor padrão padrão é definido especificamente como sendo o mesmo que um construtor padrão definido pelo usuário sem lista de inicialização e uma instrução composta vazia.

§12.1 / 6 [class.ctor] Um construtor padrão que é padronizado e não definido como excluído é implicitamente definido quando é usado por odr para criar um objeto de seu tipo de classe ou quando é explicitamente padronizado após sua primeira declaração. O construtor padrão definido implicitamente executa o conjunto de inicializações da classe que seria executado por um construtor padrão escrito pelo usuário para essa classe sem o inicializador de ctor (12.6.2) e uma instrução composta vazia. [...]

No entanto, enquanto os dois construtores se comportam da mesma forma, fornecer uma implementação vazia afeta algumas propriedades da classe. Fornecer um construtor definido pelo usuário, mesmo que não faça nada, torna o tipo não agregado e também trivial . Se você deseja que sua classe seja um tipo agregado ou trivial (ou por transitividade, um tipo de POD), será necessário usá-lo = default.

§8.5.1 / 1 [dcl.init.aggr] Um agregado é uma matriz ou uma classe sem construtores fornecidos pelo usuário, [e ...]

§12.1 / 5 [class.ctor] Um construtor padrão é trivial se não for fornecido pelo usuário e [...]

§9 / 6 [classe] Uma classe trivial é uma classe que possui um construtor padrão trivial e [...]

Para demonstrar:

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

Além disso, a inadimplência explícita de um construtor o fará constexprse o construtor implícito tivesse sido e também fornecerá a mesma especificação de exceção que o construtor implícito teria. No caso que você forneceu, o construtor implícito não seria constexpr(porque deixaria um membro de dados não inicializado) e também teria uma especificação de exceção vazia, portanto, não há diferença. Mas sim, no caso geral, você pode especificar manualmente constexpre a especificação de exceção para corresponder ao construtor implícito.

O uso = defaulttraz alguma uniformidade, pois também pode ser usado com construtores e destruidores de copiar / mover. Um construtor de cópia vazio, por exemplo, não fará o mesmo que um construtor de cópia padrão (que executará uma cópia de membro de seus membros). O uso uniforme da sintaxe = default(ou = delete) para cada uma dessas funções-membro especiais facilita a leitura do código, declarando explicitamente sua intenção.


Quase. 12.1 / 6: "Se esse construtor padrão escrito pelo usuário atender aos requisitos de um constexprconstrutor (7.1.5), o construtor padrão definido implicitamente é constexpr".
Casey #

Na verdade, 8.4.2 / 2 é mais informativo: "Se uma função é explicitamente padronizada em sua primeira declaração, (a) é implicitamente considerada como constexprse a declaração implícita fosse, (b) é implicitamente considerada como tendo a mesma declaração. especificação de exceção como se tivesse sido implicitamente declarada (15.4), ... "Não faz diferença neste caso específico, mas em geral foo() = default;tem uma pequena vantagem sobre foo() {}.
Casey #

2
Você diz que não há diferença e depois explica as diferenças?

@hvd Nesse caso, não há diferença, porque a declaração implícita não seria constexpr(uma vez que um membro de dados é deixado não inicializado) e sua especificação de exceção permite todas as exceções. Eu vou deixar isso mais claro.
Joseph Mansfield

2
Obrigado pelo esclarecimento. Ainda parece haver uma diferença, porém, com constexpr(o que você mencionou não deve fazer diferença aqui): 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 {};Somente s1dá um erro, não s2. Tanto no clang como no g ++.

10

Eu tenho um exemplo que mostrará a diferença:

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

Resultado:

5
5
0

Como podemos ver, a chamada para o construtor A () vazio não inicializa os membros, enquanto B () o faz.


7
por favor, explique esta sintaxe -> new (& x) A ();
Vencat 9/09/19

5
Estamos criando um novo objeto na memória iniciado a partir do endereço da variável x (em vez da nova alocação de memória). Essa sintaxe é usada para criar objetos na memória pré-alocada. Como no nosso caso, o tamanho de B = o tamanho de int, então new (& x) A () criará um novo objeto no lugar da variável x.
Slavenskij

Obrigado pela sua explicação.
Vencat 10/09/19

1
Eu obtenho resultados diferentes com o gcc 8.3: ideone.com/XouXux
Adam.Er8

Mesmo com o C ++ 14, estou obtendo resultados diferentes: ideone.com/CQphuT
Bhushan

9

O n2210 fornece alguns motivos:

O gerenciamento de padrões tem vários problemas:

  • As definições do construtor são acopladas; declarar qualquer construtor suprime o construtor padrão.
  • O padrão do destruidor é inadequado para as classes polimórficas, exigindo uma definição explícita.
  • Depois que um padrão é suprimido, não há como ressuscitá-lo.
  • As implementações padrão geralmente são mais eficientes do que as implementações especificadas manualmente.
  • Implementações não padrão não são triviais, o que afeta a semântica de tipos, por exemplo, torna um tipo não POD.
  • Não há como proibir uma função de membro especial ou operador global sem declarar um substituto (não trivial).

type::type() = default;
type::type() { x = 3; }

Em alguns casos, o corpo da classe pode mudar sem exigir uma alteração na definição da função de membro porque o padrão muda com a declaração de membros adicionais.

Veja Regra de três torna-se regra de cinco com C ++ 11? :

Observe que o construtor move e o operador de atribuição de movimentação não serão gerados para uma classe que declare explicitamente nenhuma das outras funções-membro especiais, que o construtor de cópia e o operador de atribuição de cópia não serão gerados para uma classe que declare explicitamente um construtor ou movimentação de movimento operador de atribuição e que uma classe com um destruidor declarado explicitamente e um construtor de cópias definido implicitamente ou um operador de atribuição de cópia definido implicitamente seja considerada obsoleta


1
São razões para ter = defaultem geral, e não razões para fazer = defaultem um construtor versus fazer { }.
Joseph Mansfield

@JosephMansfield É verdade, mas desde {}já era uma característica da linguagem antes da introdução de =default, estas razões não confiar implicitamente na distinção (por exemplo, "não há meios para ressuscitar [a padrão suprimido]" implica que {}é não equivalente ao padrão )
Kyle Strand

7

É uma questão de semântica em alguns casos. Não é muito óbvio com os construtores padrão, mas se torna óbvio com outras funções-membro geradas pelo compilador.

Para o construtor padrão, seria possível fazer com que qualquer construtor padrão com um corpo vazio fosse considerado candidato a um construtor trivial, assim como o uso =default. Afinal, os antigos construtores padrão vazios eram legais em C ++ .

struct S { 
  int a; 
  S() {} // legal C++ 
};

Se o compilador entende ou não esse construtor como trivial é irrelevante na maioria dos casos, fora das otimizações (manuais ou do compilador).

No entanto, essa tentativa de tratar os corpos de funções vazios como "padrão" é totalmente quebrada para outros tipos de funções de membro. Considere o construtor de cópia:

struct S { 
  int a; 
  S() {}
  S(const S&) {} // legal, but semantically wrong
};

No caso acima, o construtor de cópias gravado com um corpo vazio agora está errado . Na verdade, não está mais copiando nada. Esse é um conjunto de semântica muito diferente da semântica padrão do construtor de cópias. O comportamento desejado requer que você escreva algum código:

struct S { 
  int a; 
  S() {}
  S(const S& src) : a(src.a) {} // fixed
};

Mesmo com esse caso simples, no entanto, está se tornando muito mais difícil para o compilador verificar se o construtor de cópias é idêntico ao que ele geraria ou para ver que o construtor de cópias é trivial (equivalente a ummemcpy , basicamente ) O compilador precisaria verificar a expressão de cada membro inicializador e garantir que seja idêntico à expressão para acessar o membro correspondente da fonte e nada mais, garantir que nenhum membro seja deixado com uma construção padrão não trivial etc. É um processo inverso o compilador usaria para verificar se suas próprias versões geradas dessa função são triviais.

Considere então o operador de atribuição de cópias, que pode ficar ainda mais complicado, especialmente no caso não trivial. É uma tonelada de caldeira que você não quer escrever para muitas classes, mas de qualquer maneira é obrigado a fazê-lo no 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;
};

Esse é um caso simples, mas já é mais código do que você gostaria de ser forçado a escrever para um tipo tão simples como T(especialmente quando lançamos as operações para o mix). Não podemos confiar em um corpo vazio que significa "preencher os padrões" porque o corpo vazio já é perfeitamente válido e tem um significado claro. De fato, se o corpo vazio fosse usado para indicar "preencher os padrões", não haveria maneira de criar explicitamente um construtor de cópia não operacional ou algo semelhante.

É novamente uma questão de consistência. O corpo vazio significa "não faça nada", mas para coisas como construtores de cópias você realmente não quer "não faça nada", mas "faça todas as coisas que você faria normalmente se não fosse suprimido". Por isso =default. É necessário superar funções-membro suprimidas geradas pelo compilador, como copiar / mover construtores e operadores de atribuição. É então "óbvio" fazê-lo funcionar também para o construtor padrão.

Poderia ter sido bom tornar o construtor padrão com corpos vazios e os construtores triviais de membros / base também sejam considerados triviais, da mesma forma que teriam sido =defaultse tornassem o código antigo mais ideal em alguns casos, mas a maioria dos códigos de baixo nível baseados em triviais construtores padrão para otimizações também contam com construtores de cópia triviais. Se você precisar "consertar" todos os seus construtores de cópias antigos, também não é muito difícil consertar todos os seus construtores padrão antigos. Também é muito mais claro e óbvio, usando um explícito =defaultpara denotar suas intenções.

Existem algumas outras coisas que as funções-membro geradas pelo compilador farão, que você também teria que fazer explicitamente alterações no suporte. O suporte constexprpara construtores padrão é um exemplo. É mais fácil de usar mentalmente do =defaultque ter que marcar funções com todas as outras palavras-chave especiais implícitas =defaulte esse era um dos temas do C ++ 11: tornar a linguagem mais fácil. Ele ainda tem muitas verrugas e compromissos de compatibilidade traseira, mas é claro que é um grande passo à frente do C ++ 03 quando se trata de facilidade de uso.


Eu tive um problema que eu esperava = defaultque faria a=0;e não era! Eu tive que desistir disso : a(0). Ainda estou confuso sobre o quão útil = defaulté essa, é sobre desempenho? vai quebrar em algum lugar se eu simplesmente não usar = default? Eu tentei ler todas as respostas aqui comprar Eu sou novo em algumas coisas em c ++ e estou tendo muitos problemas para entendê-lo.
Poder de Aquário

@AquariusPower: não é "apenas" sobre desempenho, mas também é necessário em alguns casos, em torno de exceções e outras semânticas. Nomeadamente, um operador padrão pode ser trivial, mas um operador não padrão nunca pode ser trivial, e algum código usará técnicas de metaprogramação para alterar o comportamento ou até mesmo impedir tipos com operações não triviais. Seu a=0exemplo é devido ao comportamento de tipos triviais, que são um tópico separado (embora relacionado).
Sean Middleditch

isso significa que é possível ter = defaulte ainda conceder aserá =0? de algum modo? você acha que eu poderia criar uma nova pergunta como "como ter um construtor = defaulte conceder que os campos sejam inicializados corretamente?", btw Eu tive o problema em um structe não umclass , eo aplicativo está sendo executado corretamente, mesmo não usando = default, eu posso adicione uma estrutura mínima a essa pergunta, se ela for boa :) #
Aquarius Power

1
@AquariusPower: você pode usar inicializadores de membros de dados não estáticos. Escreva sua estrutura da seguinte maneira: struct { int a = 0; };Se você decidir que precisa de um construtor, poderá padronizá-lo, mas observe que o tipo não será trivial (o que é bom).
Sean Middleditch

2

Devido à reprovação std::is_pode à sua alternativa std::is_trivial && std::is_standard_layout, o trecho da resposta de @JosephMansfield se torna:

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

Observe que o Ylayout ainda é padrão.

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.