Por que não consigo inicializar um membro estático não const ou uma matriz estática na classe?

116

Por que não consigo inicializar um staticmembro não const ou staticmatriz em uma classe?

class A
{
    static const int a = 3;
    static int b = 3;
    static const int c[2] = { 1, 2 };
    static int d[2] = { 1, 2 };
};

int main()
{
    A a;

    return 0;
}

o compilador emite os seguintes erros:

g++ main.cpp
main.cpp:4:17: error: ISO C++ forbids in-class initialization of non-const static member b
main.cpp:5:26: error: a brace-enclosed initializer is not allowed here before ‘{’ token
main.cpp:5:33: error: invalid in-class initialization of static data member of non-integral type const int [2]’
main.cpp:6:20: error: a brace-enclosed initializer is not allowed here before ‘{’ token
main.cpp:6:27: error: invalid in-class initialization of static data member of non-integral type int [2]’

Eu tenho duas perguntas:

  1. Por que não consigo inicializar staticmembros de dados na classe?
  2. Por que não consigo inicializar staticmatrizes na classe, mesmo a constmatriz?
Yishu Fang
fonte
1
Acho que o principal motivo é que é difícil acertar. Em princípio, você provavelmente poderia fazer o que está falando, mas haveria alguns efeitos colaterais estranhos. Como se o seu exemplo de array fosse permitido, então você pode conseguir o valor de A :: c [0], mas não ser capaz de passar A :: c para uma função, pois isso exigiria um endereço e tempo de compilação constantes não têm endereço. C ++ 11 habilitou parte disso com o uso de constexpr.
Vaughn Cato
Ótima pergunta e resposta feita. Link que me ajudou: msdn.microsoft.com/en-us/library/0e5kx78b.aspx
ETFovac

Respostas:

144

Por que não consigo inicializar staticmembros de dados na classe?

O padrão C ++ permite que apenas integral constante estática ou tipos de enumeração sejam inicializados dentro da classe. Este é o motivo pelo qual apode ser inicializado, enquanto outros não.

Referência:
C ++ 03 9.4.2 Membros de dados estáticos
§4

Se um membro de dados estáticos for do tipo const integral ou const enumeração, sua declaração na definição da classe pode especificar um inicializador de constante que deve obrigatoriamente ser uma expressão de constante integral (5.19). Nesse caso, o membro pode aparecer em expressões constantes integrais. O membro ainda deve ser definido em um escopo de namespace se for usado no programa e a definição de escopo de namespace não deve conter um inicializador.

O que são tipos integrais?

C ++ 03 3.9.1 Tipos fundamentais
§7

Os tipos bool, char, wchar_t e os tipos inteiros com e sem sinal são chamados coletivamente de tipos integrais.43) Um sinônimo para o tipo integral é o tipo inteiro.

Nota de rodapé:

43) Portanto, as enumerações (7.2) não são integrais; entretanto, as enumerações podem ser promovidas a int, unsigned int, long ou unsigned long, conforme especificado em 4.5.

Gambiarra:

Você poderia usar o truque enum para inicializar um array dentro da definição de sua classe.

class A 
{
    static const int a = 3;
    enum { arrsize = 2 };

    static const int c[arrsize] = { 1, 2 };

};

Por que o padrão não permite isso?

Bjarne explica isso apropriadamente aqui :

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 do vinculador, C ++ exige que cada objeto tenha uma definição exclusiva. Essa regra seria quebrada se C ++ permitisse a definição na classe de entidades que precisavam ser armazenadas na memória como objetos.

Por que apenas static consttipos integrais e enums são permitidos na inicialização em classe?

A resposta está oculta na citação de Bjarne, lida com atenção:
"C ++ requer que cada objeto tenha uma definição única. Essa regra seria quebrada se C ++ permitisse a definição em classe de entidades que precisavam ser armazenadas na memória como objetos."

Observe que apenas static constinteiros podem ser tratados como constantes de tempo de compilação. O compilador sabe que o valor inteiro não mudará a qualquer momento e, portanto, pode aplicar sua própria magia e aplicar otimizações, o compilador simplesmente inline esses membros da classe, ou seja, eles não são mais armazenados na memória, pois a necessidade de serem armazenados na memória é removida , dá a tais variáveis ​​a exceção à regra mencionada por Bjarne.

É importante notar aqui que mesmo que os static constvalores integrais possam ter inicialização em classe, tomar o endereço de tais variáveis ​​não é permitido. Pode-se obter o endereço de um membro estático se (e somente se) ele tiver uma definição fora da classe. Isso valida ainda mais o raciocínio acima.

enums são permitidos porque valores de um tipo enumerado podem ser usados ​​onde ints são esperados. veja a citação acima


Como isso muda no C ++ 11?

C ++ 11 relaxa a restrição até certo ponto.

C ++ 11 9.4.2 Membros de dados estáticos
§3

Se um membro de dados estáticos for do tipo const literal, sua declaração na definição de classe pode especificar um inicializador de chave ou igual no qual cada cláusula de inicialização que é uma expressão de atribuição é uma expressão constante. Um membro de dados estáticos de tipo literal pode ser declarado na definição de classe com o, constexpr specifier;se assim for, sua declaração deve especificar um inicializador de chave ou igual no qual cada cláusula de inicialização que é uma expressão de atribuiçãoé uma expressão constante. [Nota: Em ambos os casos, o membro pode aparecer em expressões constantes. —End note] O membro ainda deve ser definido em um escopo de namespace se for usado no programa e a definição de escopo de namespace não deve conter um inicializador.

Além disso, C ++ 11 vai permitir (§12.6.2.8) um membro de dados não-estático para ser inicializado em que é declarado (na sua classe). Isso significa uma semântica do usuário muito fácil.

Observe que esses recursos ainda não foram implementados no último gcc 4.7, então você ainda pode obter erros de compilação.

Alok Save
fonte
7
As coisas são diferentes em c ++ 11. A resposta pode usar atualização.
bames53
4
Isso não parece ser verdade: "Observe que apenas inteiros constantes estáticos podem ser tratados como constantes de tempo de compilação. O compilador sabe que o valor inteiro não mudará a qualquer momento e, portanto, pode aplicar sua própria magia e aplicar otimizações, o compilador simplesmente alinha esses membros da classe, ou seja, eles não são mais armazenados na memória , " Tem certeza de que eles são necessariamente armazenados na memória? E se eu fornecer definições para os membros? O que &membervoltaria?
Nawaz
2
@Als: Sim. Essa é a minha pergunta. Então, por que o C ++ permite a inicialização em classe apenas para tipos integrais, sua resposta não respondeu corretamente. Pense em por que não permite a inicialização para o static const char*membro?
Nawaz
3
@Nawaz: Como o C ++ 03 só permitia o inicializador constante para o tipo de enumeração estática e const integral e const e nenhum outro tipo, o C ++ 11 estende isso para um tipo const literal que relaxa as normas para inicialização em classe. em C ++ 03 foi talvez um descuido que justificou uma mudança e, portanto, foi corrigido em C ++ 11, se houver quaisquer razões táticas tradicionais para a mudança, eu não estou ciente delas. Se você estiver ciente de alguma, sinta-se à vontade para compartilhar eles.
Alok Save
4
A "solução alternativa" que você mencionou não está funcionando com o g ++.
iammilind
4

Isso parece uma relíquia dos velhos tempos dos linkers simples. Você pode usar variáveis ​​estáticas em métodos estáticos como solução alternativa:

// header.hxx
#include <vector>

class Class {
public:
    static std::vector<int> & replacement_for_initialized_static_non_const_variable() {
        static std::vector<int> Static {42, 0, 1900, 1998};
        return Static;
    }
};

int compilation_unit_a();

e

// compilation_unit_a.cxx
#include "header.hxx"

int compilation_unit_a() {  
    return Class::replacement_for_initialized_static_non_const_variable()[1]++;
}

e

// main.cxx
#include "header.hxx"

#include <iostream>

int main() {
    std::cout
    << compilation_unit_a()
    << Class::replacement_for_initialized_static_non_const_variable()[1]++
    << compilation_unit_a()
    << Class::replacement_for_initialized_static_non_const_variable()[1]++
    << std::endl;
}

Construir:

g++ -std=gnu++0x -save-temps=obj -c compilation_unit_a.cxx 
g++ -std=gnu++0x -o main main.cxx compilation_unit_a.o

corre:

./main

O fato de que isso funciona (consistentemente, mesmo se a definição da classe estiver incluída em unidades de compilação diferentes), mostra que o vinculador hoje (gcc 4.9.2) é realmente inteligente o suficiente.

Engraçado: impressões 0123no braço e 3210no x86.

não um usuário
fonte
1

Acho que é para evitar que você misture declarações e definições. (Pense nos problemas que podem ocorrer se você incluir o arquivo em vários lugares.)

user541686
fonte
0

É porque só pode haver uma definição de A::a que todas as unidades de tradução usam.

Se você executou static int a = 3;em uma classe em um cabeçalho incluído em todas as unidades de tradução, você obterá várias definições. Portanto, a definição não fora de linha de uma estática é forçosamente transformada em um erro do compilador.

Usando static inlineou static constcorrige isso. static inlinesó concretiza o símbolo se for usado na unidade de tradução e garante que o vinculador só selecione e deixe uma cópia se estiver definido em várias unidades de tradução por estar em um grupo comdat. constno escopo do arquivo faz com que o compilador nunca emita um símbolo porque ele sempre é substituído imediatamente no código, a menos que externseja usado, o que não é permitido em uma classe.

Uma coisa a observar é que static inline int b;é tratado como uma definição, enquanto static const int bou static const A b;ainda é tratado como uma declaração e deve ser definido fora da linha se você não defini-lo dentro da classe. Curiosamente, static constexpr A b;é tratado como uma definição, enquanto static constexpr int b;é um erro e deve ter um inicializador (isso porque agora se tornam definições e como qualquer definição const / constexpr no escopo do arquivo, eles requerem um inicializador que um int não tem, mas um tipo de classe faz porque tem um implícito = A()quando é uma definição - o clang permite isso, mas o gcc requer que você inicialize explicitamente ou é um erro. Em vez disso, não é um problema com inline). static const A b = A();não é permitido e deve ser constexprouinlinea fim de permitir um inicializador para um objeto estático com tipo de classe, isto é, fazer um membro estático do tipo de classe mais do que uma declaração. Portanto, sim, em certas situações, A a;não é o mesmo que inicializar explicitamente A a = A();(o primeiro pode ser uma declaração, mas se apenas uma declaração for permitida para esse tipo, o último é um erro. O último só pode ser usado em uma definição. constexprTorna-o uma definição ) Se você usar constexpre especificar um construtor padrão, então o construtor precisará serconstexpr

#include<iostream>

struct A
{
    int b =2;
    mutable int c = 3; //if this member is included in the class then const A will have a full .data symbol emitted for it on -O0 and so will B because it contains A.
    static const int a = 3;
};

struct B {
    A b;
    static constexpr A c; //needs to be constexpr or inline and doesn't emit a symbol for A a mutable member on any optimisation level
};

const A a;
const B b;

int main()
{
    std::cout << a.b << b.b.b;
    return 0;
}

Um membro estático é uma declaração de escopo de arquivo definitiva extern int A::a;(que só pode ser feita na classe e as definições fora de linha devem se referir a um membro estático em uma classe e devem ser definições e não podem conter extern) enquanto um membro não estático faz parte de a definição de tipo completa de uma classe e têm as mesmas regras que as declarações de escopo de arquivo sem extern. Eles são definições implicitamente. Portanto, int i[]; int i[5];é uma redefinição, ao passo que static int i[]; int A::i[5];não é, mas ao contrário de 2 externos, o compilador ainda detectará um membro duplicado se você fizer isso static int i[]; static int i[5];na classe.

Lewis Kelsey
fonte
-3

variáveis ​​estáticas são específicas de uma classe. Construtores inicializam atributos ESPECIALY para uma instância.


fonte