Variável de membro estático C ++ e sua boot

Para variables ​​de membro estático na class C ++ – a boot é feita fora da class. Eu quero saber porque? Qualquer raciocínio / restrição lógica para isso? Ou é uma implementação puramente legada – que a norma não quer corrigir?

Eu acho que ter boot na class é mais “intuitivo” e menos confuso. Também dá a sensação de estática e globalidade da variável. Por exemplo, se você vir o membro const estático.

Fundamentalmente, isso ocorre porque os membros estáticos devem ser definidos em exatamente uma unidade de tradução, para não violar a regra de uma definição . Se a linguagem permitir algo como:

struct Gizmo { static string name = "Foo"; }; 

então o name seria definido em cada unidade de tradução que #include este arquivo de header.

O C ++ permite que você defina membros estáticos integrais dentro da declaração, mas você ainda precisa include uma definição em uma única unidade de tradução, mas isso é apenas um atalho ou um acento sintático. Então, isso é permitido:

 struct Gizmo { static const int count = 42; }; 

Contanto que a) a expressão seja const integral ou um tipo de enumeração, b) a expressão possa ser avaliada em tempo de compilation, ec) ainda haja uma definição em algum lugar que não viole a regra de uma definição:

arquivo: gizmo.cpp

 #include "gizmo.h" const int Gizmo::count; 

Em C ++ desde o início dos tempos, a presença de um inicializador era um atributo exclusivo da definição de object, isto é , uma declaração com um inicializador é sempre uma definição (quase sempre).

Como você deve saber, cada object externo usado no programa C ++ deve ser definido uma vez e somente uma vez em apenas uma unidade de tradução. Permitir inicializadores em class para objects estáticos iria imediatamente contra essa convenção: os inicializadores entrariam em arquivos de header (onde as definições de class geralmente residem) e assim gerariam várias definições do mesmo object estático (uma para cada unidade de tradução que inclui o arquivo de header ). Isto é, obviamente, inaceitável. Por essa razão, a abordagem de declaração para membros estáticos da class é deixada perfeitamente “tradicional”: você a declara apenas no arquivo de header (ou seja, nenhum inicializador é permitido) e a define em uma unidade de tradução de sua escolha (possivelmente com um inicializador ).

Uma exceção dessa regra foi feita para membros de class estáticos const de tipos integral ou enum, porque essas inputs podem ser para Expressões Constantes Integrais (ICEs). A idéia principal dos ICEs é que eles são avaliados em tempo de compilation e, portanto, não dependem das definições dos objects envolvidos. É por isso que essa exceção foi possível para os tipos integral ou enum. Mas para outros tipos, isso apenas contradiz os princípios básicos de declaração / definição de C ++.

É por causa da maneira como o código é compilado. Se você fosse inicializá-lo na class, que geralmente está no header, toda vez que o header for incluído, você obterá uma instância da variável estática. Esta definitivamente não é a intenção. Ter inicializado fora da class dá a você a possibilidade de inicializá-lo no arquivo cpp.

Seção 9.4.2, Membros de dados estáticos, dos estados do padrão C ++:

Se um membro de dados static for const integral ou um tipo de enumeração const , sua declaração na definição de class poderá especificar um inicializador const que deverá ser uma expressão constante integral.

Portanto, é possível que o valor de um membro de dados estáticos seja incluído “dentro da class” (pelo que presumo que você queira dizer dentro da declaração da class). No entanto, o tipo de membro de dados estáticos deve ser um tipo de enumeração const ou const . A razão pela qual os valores de membros de dados estáticos de outros tipos não podem ser especificados na declaração de class é que a boot não-trivial é provavelmente necessária (isto é, um construtor precisa ser executado).

Imagine se os seguintes fossem legais:

 // my_class.hpp #include  class my_class { public: static std::string str = "static std::string"; //... 

Cada arquivo de object correspondente aos arquivos CPP que incluem este header não teria apenas uma cópia do espaço de armazenamento para my_class::str (consistindo de bytes sizeof(std::string) ), mas também uma “seção ctor” que chama o std::string Construtor de std::string tomando uma string C Cada cópia do espaço de armazenamento para my_class::str seria identificada por um label comum, portanto, um vinculador poderia, teoricamente, mesclar todas as cópias do espaço de armazenamento em uma única. No entanto, um vinculador não seria capaz de isolar todas as cópias do código do construtor nas seções ctor dos arquivos de object. Seria como pedir ao vinculador para remover todo o código para inicializar str na compilation do seguinte:

 std::map map; std::vector vec; std::string str = "test"; int c = 99; my_class mc; std::string str2 = "test2"; 

EDIT É instrutivo examinar a saída do assembler do g ++ para o seguinte código:

 // SO4547660.cpp #include  class my_class { public: static std::string str; }; std::string my_class::str = "static std::string"; 

O código assembly pode ser obtido executando:

 g++ -S SO4547660.cpp 

Olhando através do arquivo SO4547660.s que g + + gera, você pode ver que há um monte de código para um arquivo de origem tão pequeno.

__ZN8my_class3strE é o label do espaço de armazenamento para my_class::str . Há também a fonte assembly de uma function __static_initialization_and_destruction_0(int, int) , que possui o label __Z41__static_initialization_and_destruction_0ii . Essa function é especial para g ++, mas apenas saiba que o g ++ irá certificar-se de que ele seja chamado antes de qualquer código não-inicializador ser executado. Observe que a implementação dessa function chama __ZNSsC1EPKcRKSaIcE . Este é o símbolo desconfigurado para std::basic_string, std::allocator >::basic_string(char const*, std::allocator const&) .

Voltando ao exemplo hipotético acima e usando esses detalhes, cada arquivo de object correspondente a um arquivo CPP que inclui my_class.hpp teria o label __ZN8my_class3strE para sizeof(std::string) bytes, bem como o código assembly para chamar __ZNSsC1EPKcRKSaIcE dentro de sua implementação da function __static_initialization_and_destruction_0(int, int) . O vinculador pode facilmente mesclar todas as ocorrências de __ZN8my_class3strE , mas não é possível isolar o código que chama __ZNSsC1EPKcRKSaIcE dentro da implementação do arquivo object de __static_initialization_and_destruction_0(int, int) .

Eu acho que o principal motivo para ter a boot feita fora do bloco de class é permitir a boot com valores de retorno de outras funções de membro de class. Se você quisesse inicializar a::var com b::some_static_fn() você precisaria ter certeza de que todo arquivo .cpp que inclua ah inclua bh primeiro. Seria uma bagunça, especialmente quando (mais cedo ou mais tarde) você se deparasse com uma referência circular que só poderia ser resolvida com uma interface desnecessária. O mesmo problema é a principal razão para ter implementações de function de membro de class em um arquivo .cpp vez de colocar tudo em sua class principal ‘ .h .

Pelo menos com funções de membro você tem a opção de implementá-las no header. Com variables, você deve fazer a boot em um arquivo .cpp. Eu não concordo totalmente com a limitação, e também não acho que haja uma boa razão para isso.