Antes do C ++ 11, só podíamos realizar a inicialização em classe em membros const estáticos do tipo integral ou enumeração. Stroustrup discute isso em seu C ++ FAQ , dando o seguinte exemplo:
class Y {
const int c3 = 7; // error: not static
static int c4 = 7; // error: not const
static const float c5 = 7; // error: not integral
};
E o seguinte raciocínio:
Então, por que essas restrições inconvenientes existem? Uma classe é normalmente declarada em um arquivo de cabeçalho e um arquivo de cabeçalho é normalmente incluído em muitas unidades de tradução. No entanto, para evitar regras complicadas de vinculador, C ++ exige que cada objeto tenha uma definição exclusiva. Essa regra seria quebrada se o C ++ permitisse a definição em classe de entidades que precisavam ser armazenadas na memória como objetos.
No entanto, o C ++ 11 relaxa essas restrições, permitindo a inicialização em classe de membros não estáticos (§12.6.2 / 8):
Em um construtor não delegante, se um determinado membro de dados não estáticos ou classe base não for designado por um mem-initializer-id (incluindo o caso em que não há mem-initializer-list porque o construtor não tem ctor-initializer ) e a entidade não é uma classe base virtual de uma classe abstrata (10.4), então
- se a entidade for um membro de dados não estático que possui um inicializador de chave ou igual , a entidade será inicializada conforme especificado em 8.5;
- caso contrário, se a entidade for um membro variante (9.5), nenhuma inicialização é realizada;
- caso contrário, a entidade é inicializada por padrão (8.5).
A seção 9.4.2 também permite a inicialização em classe de membros estáticos não constantes se eles estiverem marcados com o constexpr
especificador.
Então, o que aconteceu com as razões para as restrições que tínhamos em C ++ 03? Simplesmente aceitamos as "regras complicadas do vinculador" ou alguma outra coisa mudou que torna isso mais fácil de implementar?
fonte
Respostas:
A resposta curta é que eles mantiveram o vinculador quase o mesmo, à custa de tornar o compilador ainda mais complicado do que antes.
Ou seja, em vez de resultar em várias definições para o vinculador classificar, ainda resulta apenas em uma definição, e o compilador tem que classificá-la.
Também leva a regras um pouco mais complexas para o programador manter ordenadas também, mas é mais simples o suficiente para não ser um grande problema. As regras extras surgem quando você tem dois inicializadores diferentes especificados para um único membro:
class X { int a = 1234; public: X() = default; X(int z) : a(z) {} };
Agora, as regras extras neste ponto lidam com qual valor é usado para inicializar
a
quando você usa o construtor não padrão. A resposta para isso é bastante simples: se você usar um construtor que não especifica nenhum outro valor, o1234
seria usado para inicializara
- mas se você usar um construtor que especifica algum outro valor, o1234
é basicamente ignorado.Por exemplo:
#include <iostream> class X { int a = 1234; public: X() = default; X(int z) : a(z) {} friend std::ostream &operator<<(std::ostream &os, X const &x) { return os << x.a; } }; int main() { X x; X y{5678}; std::cout << x << "\n" << y; return 0; }
Resultado:
1234 5678
fonte
Acho que esse raciocínio pode ter sido escrito antes que os modelos fossem finalizados. Afinal, as "regras complicadas de vinculador" necessárias para inicializadores em classe de membros estáticos eram / já eram necessárias para o C ++ 11 oferecer suporte a membros estáticos de modelos.
Considerar
struct A { static int s = ::ComputeSomething(); }; // NOTE: This isn't even allowed, // thanks @Kapil for pointing that out // vs. template <class T> struct B { static int s; } template <class T> int B<T>::s = ::ComputeSomething(); // or template <class T> void Foo() { static int s = ::ComputeSomething(); s++; std::cout << s << "\n"; }
O problema para o compilador é o mesmo em todos os três casos: em qual unidade de tradução ele deve emitir a definição
s
e o código necessário para inicializá-la? A solução simples é emiti-lo em todos os lugares e deixar o linker resolvê-lo. É por isso que os vinculadores já suportam coisas como__declspec(selectany)
. Simplesmente não teria sido possível implementar C ++ 03 sem ele. E é por isso que não foi necessário estender o vinculador.Para ser mais direto: acho que o raciocínio dado no padrão antigo está simplesmente errado.
ATUALIZAR
Como Kapil apontou, meu primeiro exemplo nem mesmo é permitido no padrão atual (C ++ 14). Deixei de qualquer maneira, porque IMO é o caso mais difícil para a implementação (compilador, vinculador). Meu ponto é: mesmo nesse caso, não é mais difícil do que o que já é permitido, por exemplo, ao usar modelos.
fonte
Em teoria, a
So why do these inconvenient restrictions exist?...
razão é válida, mas pode ser facilmente contornada e é exatamente isso que o C ++ 11 faz.Quando você inclui um arquivo, ele simplesmente inclui o arquivo e desconsidera qualquer inicialização. Os membros são inicializados apenas quando você instancia a classe.
Em outras palavras, a inicialização ainda está vinculada ao construtor, apenas a notação é diferente e é mais conveniente. Se o construtor não for chamado, os valores não serão inicializados.
Se o construtor for chamado, os valores serão inicializados com a inicialização na classe, se houver, ou o construtor poderá substituir isso pela própria inicialização. O caminho de inicialização é essencialmente o mesmo, ou seja, por meio do construtor.
Isso é evidente no próprio FAQ do Stroustrup sobre C ++ 11.
fonte
Y::c3
pergunta? Pelo que entendi,c3
sempre será inicializado, a menos que haja um construtor que substitua o padrão fornecido na declaração.