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

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

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

Foi proibido por razões históricas?

A minha principal questão é, isso afetará alguma funcionalidade se static data members forem declarados / definidos ao mesmo tempo (o mesmo que Java )?

    
por iammilind 20.04.2012 / 07:12
fonte

7 respostas

15

Acho que a limitação que você considerou não está relacionada à semântica (por que algo deveria mudar se a inicialização fosse definida no mesmo arquivo?), mas ao modelo de compilação C ++ que, por razões de retrocompatibilidade, não pode ser facilmente alterado porque se tornaria muito complexo (suportando um novo modelo de compilação e o existente ao mesmo tempo) ou não permitiria compilar o código existente (introduzindo um novo modelo de compilação e eliminando o modelo existente).

O modelo de compilação C ++ deriva de C, no qual você importa declarações em um arquivo de origem incluindo arquivos (cabeçalho). Dessa maneira, o compilador vê exatamente um grande arquivo de origem, contendo todos os arquivos incluídos, e todos os arquivos incluídos nesses arquivos, de forma recursiva. Isto tem IMO uma grande vantagem, ou seja, que torna o compilador mais fácil de implementar. Claro, você pode escrever qualquer coisa nos arquivos incluídos, ou seja, declarações e definições. É 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 sabe muito bem se está importando a declaração de um símbolo global que é definido em outro módulo , ou se estiver compilando a definição de um símbolo global fornecido pelo módulo atual . Somente no último caso, o compilador deve colocar este símbolo (por exemplo, uma variável) na corrente arquivo de objeto.

Por exemplo, no GNU Pascal você pode escrever uma unidade a em um arquivo a.pas assim:

unit a;

interface

var MyStaticVariable: Integer;

implementation

begin
  MyStaticVariable := 0
end.

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

Em seguida, você pode ter unidades diferentes que importam e usam a variável global MyStaticVariable , por ex. 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 b e c 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 produz um executável com:

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

e execute-o:

$ ./m
1
2
3

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

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 . Assim, todas as definições e inicializações para a classe A podem ser colocadas em um arquivo de origem.

Voltando ao C ++, o motivo pelo qual em C ++ você precisa definir membros de dados estáticos em um arquivo separado é mais relacionado ao modelo de compilação C ++ do que às limitações impostas pelo vinculador ou outras ferramentas usadas pelo compilador. Em C ++, importar alguns símbolos significa construir sua declaração como parte de a 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 quaisquer símbolos globais (funções, variáveis, métodos, membros de dados estáticos) em um arquivo incluído, caso contrário, esses símbolos podem ser multiplamente definido nos arquivos de objetos compilados.

    
por 21.04.2012 / 10:58
fonte
41

Como os membros estáticos são compartilhados entre TODAS as instâncias de uma classe, eles precisam ser definidos em um e em apenas um lugar. 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 cada módulo que inclua esse cabeçalho, e você obterá erros durante a vinculação à medida que encontrar todas as definições duplicadas.

Sim, isso é, pelo menos em parte, um problema histórico datado de cfront; um compilador poderia ser escrito para criar um tipo de "static_members_of_everything.cpp" oculto e linkar para ele. No entanto, quebraria a compatibilidade retroativa, e não haveria nenhum benefício real em fazê-lo.

    
por 20.04.2012 / 07:24
fonte
6

A razão provável para isso é que isso mantém a linguagem C ++ implementável em ambientes em que o arquivo de objeto 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 uma declaração por boas razões) é inserida em várias unidades de tradução. Se a declaração continha definições para variáveis estáticas, então você acabaria 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 manipule várias definições sem reclamar.

(E note que isso entra em conflito com a Regra de Definição Única, a menos que isso possa ser feito de acordo com o tipo de símbolo ou em que tipo de seção ele é colocado.)

    
por 20.04.2012 / 08:00
fonte
6

Existe uma grande diferença entre C ++ e Java.

O 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, simplesmente agirá no mesmo objeto que o ambiente de tempo de execução conhece ultimamente.

Em C ++ não existe um "dono do conhecimento final": C ++, C, Fortran Pascal, etc. são todos "tradutores" de um código fonte (arquivo CPP) em um formato intermediário (o arquivo OBJ, ou ".o" , dependendo do sistema operacional), onde as instruções são traduzidas na instrução da 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 "linker"), que une todos os OBJ-s juntos (não importa a língua de onde eles vêm) apontando novamente todos os endereços que são para símbolos. , para a sua definição efetiva.

A propósito, o linker funciona, uma definição (o que cria o espaço físico para uma variável) deve ser única.

Observe que o C ++ não se vincula 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 usar do jeito que é.

Agora: um arquivo de cabeçalho é algo para ser "colado" em vários arquivos CPP. Todo arquivo CPP é traduzido independentemente de todos os outros. Um compilador traduzindo diferentes arquivos CPP, todos recebendo em uma mesma definição colocarão o " código de criação " para o 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 porque as definições existem e de onde elas vêm (nem sequer sabe sobre o C ++: cada "linguagem estática" pode produzir definições e referências a serem vinculadas). Ele apenas sabe que há 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 terá nenhum conhecimento (sendo agnóstico de idioma) sobre o que fazer com elas.

É como mesclar um número de cidade para formar uma cidade grande: se você encontrar dois " Time square " e um número de pessoas vindo de fora pedindo para irem para " > Time square ", você não pode decidir sobre uma base técnica pura (sem qualquer conhecimento sobre a política que atribuiu esses nomes e será responsável por gerenciá-los) em que lugar exato para enviar eles.

    
por 21.04.2012 / 09:10
fonte
5

É necessário porque, caso contrário, o compilador não sabe onde colocar a variável. Cada arquivo cpp é individualmente compilado e não sabe sobre o outro. O linker 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 a vtable está definida).

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

    
por 21.04.2012 / 05:57
fonte
2

Acho que encontrei o motivo. A definição da variável static no espaço separado permite inicializá-lo para qualquer valor. Se não for inicializado, o padrão será 0.

Antes do C ++ 11, a inicialização da classe in não era permitida em C ++. Então, um não 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::i agora é global e declarar global em muitos arquivos causa vários erros de link de símbolo.

Assim, é necessário declarar uma variável de classe static dentro de uma unidade de tradução separada. No entanto, ainda pode-se argumentar que seguir o caminho deve instruir o compilador a não criar múltiplos símbolos

static int X::i = 4;
^^^^^^
    
por 18.11.2012 / 09:00
fonte
0

A :: x é apenas uma variável global, mas tem 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, pode ser crítico.

  2. o inicializador estático - você pode deixar um cliente decidir a que A :: x deve ser inicializado.

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

Eu duvido que estes são "porque" esta situação surgiu. É provavelmente apenas uma evolução do C se transformando em C ++, e um problema de compatibilidade com versões anteriores que impede você de mudar a linguagem agora.

    
por 23.02.2017 / 07:21
fonte