Por que os membros de dados estáticos precisam ser definidos fora da classe separadamente em C ++ (diferente do Java)?

41
class A {
  static int foo () {} // ok
  static int x; // <--- needed to be defined separately in .cpp file
};

Não vejo a necessidade de ter A::xdefinido separadamente em um arquivo .cpp (ou o mesmo arquivo para modelos). Por que não pode ser A::xdeclarado e definido ao mesmo tempo?

Foi proibido por razões históricas?

Minha principal pergunta é: isso afetará alguma funcionalidade se staticos membros de dados forem declarados / definidos ao mesmo tempo (o mesmo que Java )?

iammilind
fonte
Como prática recomendada, geralmente é melhor agrupar sua variável estática em um método estático (possivelmente como uma estática local) para evitar problemas de ordem de inicialização.
Tamás Szelei
2
Essa regra é realmente relaxada um pouco no C ++ 11. membros estáticos const geralmente não precisam mais ser definidos. Veja: en.wikipedia.org/wiki/…
mirk
4
@afishwhoswimsaround: especificar regras generalizadas para todas as situações não é uma boa ideia (as práticas recomendadas devem ser aplicadas com o contexto). Aqui você está tentando resolver um problema que não existe. O problema da ordem de inicialização afeta apenas objetos que possuem construtores e acessam outros objetos de duração de armazenamento estático. Como 'x' é int, o primeiro não se aplica, já que 'x' é privado, o segundo não se aplica. Em terceiro lugar, isso não tem nada a ver com a questão.
Martin Iorque
1
Pertence ao estouro de pilha?
Lightness Races com Monica
2
C ++ 17 permite a inicialização em linha de membros de dados estáticos (mesmo para tipos não inteiros): inline static int x[] = {1, 2, 3};. Veja en.cppreference.com/w/cpp/language/static#Static_data_members
Vladimir Reshetnikov

Respostas:

15

Eu acho que a limitação que você considerou não está relacionada à semântica (por que algo deve mudar se a inicialização foi definida no mesmo arquivo?), Mas ao modelo de compilação C ++ que, por razões de compatibilidade com versões anteriores, não pode ser facilmente alterado porque ou se tornam muito complexos (suportando um novo modelo de compilação e o existente ao mesmo tempo) ou não permitem compilar o código existente (introduzindo um novo modelo de compilação e descartando o existente).

O modelo de compilação C ++ deriva do modelo C, no qual você importa declarações para um arquivo de origem, incluindo arquivos (cabeçalho). Dessa forma, o compilador vê exatamente um grande arquivo de origem, contendo todos os arquivos incluídos e todos os arquivos incluídos nesses arquivos, recursivamente. Isso tem uma grande vantagem da IMO, a saber, que facilita a implementação do compilador. Obviamente, você pode escrever qualquer coisa nos arquivos incluídos, como declarações e definições. É apenas uma boa prática colocar declarações em arquivos de cabeçalho e definições em arquivos .c ou .cpp.

Por outro lado, é possível ter um modelo de compilação no qual o compilador saiba muito bem se está importando a declaração de um símbolo global definido em outro módulo ou se está compilando a definição de um símbolo global fornecida por o módulo atual . Somente neste último caso, o compilador deve colocar esse símbolo (por exemplo, uma variável) no arquivo de objeto atual.

Por exemplo, no GNU Pascal, você pode escrever uma unidade aem um arquivo a.pascomo este:

unit a;

interface

var MyStaticVariable: Integer;

implementation

begin
  MyStaticVariable := 0
end.

onde a variável global é declarada e inicializada no mesmo arquivo de origem.

Então você pode ter unidades diferentes que importam a e usam a variável global MyStaticVariable, por exemplo, uma unidade b ( b.pas):

unit b;

interface

uses a;

procedure PrintB;

implementation

procedure PrintB;
begin
  Inc(MyStaticVariable);
  WriteLn(MyStaticVariable)
end;
end.

e uma unidade c ( c.pas):

unit c;

interface

uses a;

procedure PrintC;

implementation

procedure PrintC;
begin
  Inc(MyStaticVariable);
  WriteLn(MyStaticVariable)
end;
end.

Finalmente, você pode usar as unidades bec em um programa principal m.pas:

program M;

uses b, c;

begin
  PrintB;
  PrintC;
  PrintB
end.

Você pode compilar esses arquivos separadamente:

$ gpc -c a.pas
$ gpc -c b.pas
$ gpc -c c.pas
$ gpc -c m.pas

e depois produza um executável com:

$ gpc -o m m.o a.o b.o c.o

e execute:

$ ./m
1
2
3

O truque aqui é que, quando o compilador vê uma diretiva usos em um módulo de programa (por exemplo, usa a em b.pas), ele não inclui o arquivo .pas correspondente, mas procura um arquivo .gpi, ou seja, um arquivo pré-compilado arquivo de interface (consulte a documentação ). Esses .gpiarquivos são gerados pelo compilador junto com os .oarquivos quando cada módulo é compilado. Portanto, o símbolo global MyStaticVariableé definido apenas uma vez no arquivo de objeto a.o.

Java funciona de maneira semelhante: quando o compilador importa uma classe A para a classe B, ele procura no arquivo de classe A e não precisa do arquivo A.java. Portanto, todas as definições e inicializações da classe A podem ser colocadas em um arquivo de origem.

Voltando ao C ++, a razão pela qual no C ++ é necessário definir membros de dados estáticos em um arquivo separado está mais relacionada ao modelo de compilação do C ++ do que às limitações impostas pelo vinculador ou por outras ferramentas usadas pelo compilador. No C ++, importar alguns símbolos significa criar sua declaração como parte da unidade de compilação atual. Isso é muito importante, entre outras coisas, devido à maneira como os modelos são compilados. Mas isso implica que você não pode / não deve definir nenhum símbolo global (funções, variáveis, métodos, membros de dados estáticos) em um arquivo incluído; caso contrário, esses símbolos podem ser definidos de várias formas nos arquivos de objetos compilados.

Giorgio
fonte
42

Como os membros estáticos são compartilhados entre TODAS as instâncias de uma classe, eles precisam ser definidos em um e apenas um local. Realmente, são variáveis ​​globais com algumas restrições de acesso.

Se você tentar defini-los no cabeçalho, eles serão definidos em todos os módulos que incluem esse cabeçalho e você receberá erros durante a vinculação, pois encontra todas as definições duplicadas.

Sim, isso é pelo menos em parte uma questão histórica que data do cfront; um compilador pode ser escrito para criar uma espécie de "static_members_of_everything.cpp" oculto e vincular a isso. No entanto, isso quebraria a compatibilidade com versões anteriores e não haveria nenhum benefício real em fazê-lo.

mjfgates
fonte
2
Minha pergunta não é a razão do comportamento atual, mas a justificativa para a gramática dessa linguagem. Em outras palavras, suponha que se as staticvariáveis ​​são declaradas / definidas no mesmo local (como Java), o que pode dar errado?
precisa saber é o seguinte
8
@iammilind Acho que você não entende que a gramática é necessária por causa da explicação desta resposta. Agora porque? Devido ao modelo de compilação dos arquivos C (e C ++): c e cpp, são os arquivos de código reais que são compilados separadamente como programas separados, então eles são vinculados para tornar um executável completo. Os cabeçalhos não são realmente código para o compilador, são apenas texto para copiar e colar nos arquivos c e cpp. Agora, se algo for definido várias vezes, ele não poderá ser compilado, da mesma forma que não será compilado se você tiver várias variáveis ​​locais com o mesmo nome.
Klaim
1
@Klaim, e os staticmembros template? Eles são permitidos em todos os arquivos de cabeçalho, pois precisam estar visíveis. Não estou contestando esta resposta, mas ela também não corresponde à minha pergunta.
Iammilind 20/04
Os modelos @iammilind não são código real, são códigos que geram código. Cada instância de um modelo possui uma e apenas uma instância estática de cada declaração estática fornecida pelo compilador. Você ainda precisa definir a instância, mas como define um modelo de instância, não é um código real, como dito acima. Modelos são, literalmente, modelos de código para o compilador gerar código.
Klaim
2
@iammilind: Normalmente, os modelos são instanciados em cada arquivo de objeto, incluindo suas variáveis ​​estáticas. No Linux com arquivos de objeto ELF, o compilador marca as instâncias como símbolos fracos , o que significa que o vinculador combina várias cópias da mesma instanciação. A mesma tecnologia pode ser usada para permitir a definição de variáveis ​​estáticas nos arquivos de cabeçalho; portanto, o motivo pelo qual isso não foi feito é provavelmente uma combinação de motivos históricos e considerações de desempenho da compilação. Esperamos que todo o modelo de compilação seja corrigido quando o próximo padrão C ++ incorporar módulos .
han
6

O motivo provável para isso é que isso mantém a linguagem C ++ implementável em ambientes onde o arquivo de objetos e o modelo de ligação não suportam a mesclagem de várias definições de vários arquivos de objetos.

Uma declaração de classe (chamada de declaração por boas razões) é puxada para várias unidades de tradução. Se a declaração contivesse definições para variáveis ​​estáticas, você terminaria com várias definições em várias unidades de tradução (e lembre-se, esses nomes têm ligação externa).

Essa situação é possível, mas requer que o vinculador lide com várias definições sem reclamar.

(E observe que isso está em conflito com a Regra de Uma Definição, a menos que possa ser feita de acordo com o tipo de símbolo ou em que tipo de seção é inserida.)

Kaz
fonte
6

Há uma grande diferença entre C ++ e Java.

Java opera em sua própria máquina virtual que cria tudo em seu próprio ambiente de tempo de execução. Se uma definição for vista mais de uma vez, ela simplesmente atuará no mesmo objeto que o ambiente de tempo de execução conhece ultimamente.

No C ++, não há "proprietário do conhecimento final": C ++, C, Fortran Pascal etc. são todos "tradutores" de um código-fonte (arquivo CPP) para um formato intermediário (o arquivo OBJ ou o arquivo ".o", dependendo do OS) onde as instruções são traduzidas em instruções de máquina e os nomes se tornam endereços indiretos mediados por uma tabela de símbolos.

Um programa não é feito pelo compilador, mas por outro programa (o "vinculador"), que une todos os OBJ-s (independentemente do idioma de origem), apontando novamente todos os endereços direcionados a símbolos, em direção a seus definição efetiva.

Pela maneira como o vinculador funciona, uma definição (o que cria o espaço físico para uma variável) deve ser exclusiva.

Observe que o C ++ não é vinculado por si só e que o vinculador não é emitido pelas especificações do C ++: o vinculador existe devido à maneira como os módulos do SO são criados (geralmente em C e ASM). C ++ tem que usá-lo do jeito que está.

Agora: um arquivo de cabeçalho é algo para ser "colado" em vários arquivos CPP. Todo arquivo CPP é traduzido independentemente de qualquer outro. Um compilador traduzindo diferentes arquivos CPP, todos recebendo uma mesma definição colocará o " código de criação " do objeto definido em todos os OBJs resultantes.

O compilador não sabe (e nunca saberá) se todos esses OBJs serão usados ​​juntos para formar um único programa ou separadamente para formar diferentes programas independentes.

O vinculador não sabe como e por que as definições existem e de onde elas vêm (nem sequer conhece C ++: toda "linguagem estática" pode produzir definições e referências a serem vinculadas). Apenas sabe que existem referências a um determinado "símbolo" que é "definido" em um determinado endereço resultante.

Se houver várias definições (não confunda definições com referências) para um determinado símbolo, o vinculador não tem conhecimento (sendo independente da linguagem) sobre o que fazer com eles.

É como fundir uma cidade para formar uma cidade grande: se você encontrar duas " Time Square " e várias pessoas vindas de fora pedindo para ir para " Time Square ", não poderá decidir com base pura técnica. (sem nenhum conhecimento sobre a política que atribuiu esses nomes e será responsável por gerenciá-los) no local exato para enviá-los.

Emilio Garavaglia
fonte
3
A diferença entre Java e C ++ em relação aos símbolos globais não está conectada ao Java que possui uma máquina virtual, mas ao modelo de compilação C ++. A esse respeito, eu não colocaria Pascal e C ++ na mesma categoria. Em vez disso, agruparia C e C ++ como "linguagens nas quais as declarações importadas são incluídas e compiladas com o arquivo de origem principal" em oposição a Java e Pascal (e talvez OCaml, Scala, Ada, etc) como "linguagens nas quais o as declarações importadas são consultadas pelo compilador em arquivos pré-compilados que contêm informações sobre símbolos exportados ".
Giorgio
1
@ Giorgio: a referência ao Java pode não ser bem-vinda, mas acho que a resposta de Emilio está certa ao chegar ao essencial da questão, a saber, a fase do arquivo / vinculador do objeto após a compilação separada.
Ixache #
5

É necessário porque, caso contrário, o compilador não sabe onde colocar a variável. Cada arquivo cpp é compilado individualmente e não conhece o outro. O vinculador resolve variáveis, funções etc. Eu pessoalmente não vejo qual a diferença entre os membros vtable e static (não precisamos escolher em qual arquivo o vtable está definido).

Suponho principalmente que é mais fácil para escritores de compiladores implementá-lo dessa maneira. Vars estáticos fora da classe / estrutura existem e talvez por motivos de consistência ou porque seria "mais fácil de implementar" para os escritores de compiladores que eles definiram essa restrição nos padrões.


fonte
2

Eu acho que encontrei o motivo. Definir staticvariável em espaço separado permite inicializá-lo para qualquer valor. Se não inicializado, será padronizado como 0.

Antes do C ++ 11, a inicialização na classe não era permitida no C ++. Portanto, não se pode escrever como:

struct X
{
  static int i = 4;
};

Portanto, agora para inicializar a variável, é necessário escrevê-la fora da classe como:

struct X
{
  static int i;
};
int X::i = 4;

Como discutido em outras respostas também, int X::iagora é global e declarar global em muitos arquivos causa erro no link de vários símbolos.

Assim, é preciso declarar uma staticvariável de classe dentro de uma unidade de tradução separada. No entanto, ainda pode-se argumentar que a seguinte maneira deve instruir o compilador a não criar vários símbolos

static int X::i = 4;
^^^^^^
iammilind
fonte
0

A :: x é apenas uma variável global, mas com namespace para A e com restrições de acesso.

Alguém ainda precisa declará-lo, como qualquer outra variável global, e isso pode até ser feito em um projeto que está estaticamente vinculado ao projeto que contém o restante do código A.

Eu chamaria isso de design ruim, mas há alguns recursos que você pode explorar dessa maneira:

  1. a ordem de chamada do construtor ... Não é importante para um int, mas para um membro mais complexo que talvez acesse outras variáveis ​​estáticas ou globais, isso pode ser crítico.

  2. o inicializador estático - você pode permitir que um cliente decida em que A :: x deve ser inicializado.

  3. em c ++ e c, como você tem acesso total à memória por meio de ponteiros, a localização física das variáveis ​​é significativa. Há coisas muito impertinentes que você pode explorar com base em onde uma variável está localizada em um objeto de link.

Duvido que seja por isso que essa situação tenha surgido. Provavelmente é apenas uma evolução do C se transformando em C ++ e um problema de compatibilidade com versões anteriores que impede você de alterar o idioma agora.

James Podesta
fonte
2
este não parece oferecer nada substancial sobre pontos feitos e explicado em anteriores 6 respostas
mosquito