I think the limitation you have considered is not related to semantics (why should something change if the initialization were defined in the same file?) but rather to the C++ compilation model which, for reasons of backward compatibility, cannot be easily changed because it would either become too complex (supporting a new compilation model and the existing one at the same time) or would not allow to compile existing code (by introducing a new compilation model and dropping the existing one).
The C++ compilation model stems from that of C, in which you import declarations into a source file by including (header) files. In this way, the compiler sees exactly one big source file, containing all the included files, and all the files included from those files, recursively. This has IMO one big advantage, namely that it makes the compiler easier to implement. Of course, you can write anything in the included files, i.e. both declarations and definitions. It is only a good practice to put declarations in header files and definitions in .c or .cpp files.
On the other hand, it is possible to have a compilation model in which the compiler knows very well if it is importing the declaration of a global symbol that is defined in another module, or if it is compiling the definition of a global symbol provided by the current module. Only in the latter case the compiler must put this symbol (e.g. a variable) in the current
object file.
For example, in GNU Pascal you can write a unit a
in a file a.pas
like this:
unit a;
interface
var MyStaticVariable: Integer;
implementation
begin
MyStaticVariable := 0
end.
where the global variable is declared and initialized in the same source file.
Then you can have different units that import a and use the global variable
MyStaticVariable
, e.g. a unit b (b.pas
):
unit b;
interface
uses a;
procedure PrintB;
implementation
procedure PrintB;
begin
Inc(MyStaticVariable);
WriteLn(MyStaticVariable)
end;
end.
and a unit c (c.pas
):
unit c;
interface
uses a;
procedure PrintC;
implementation
procedure PrintC;
begin
Inc(MyStaticVariable);
WriteLn(MyStaticVariable)
end;
end.
Finally you can use units b and c in a main program m.pas
:
program M;
uses b, c;
begin
PrintB;
PrintC;
PrintB
end.
You can compile these files separately:
$ gpc -c a.pas
$ gpc -c b.pas
$ gpc -c c.pas
$ gpc -c m.pas
and then produce an executable with:
$ gpc -o m m.o a.o b.o c.o
and run it:
$ ./m
1
2
3
The trick here is that when the compiler sees a uses directive in a program module (e.g. uses a in b.pas), it does not include the corresponding .pas file,
but looks for a .gpi file, i.e. for a pre-compiled interface file
(see the documentation).
These .gpi
files are generated by the compiler together with the .o
files when each module is compiled.
So the global symbol MyStaticVariable
is only defined once in the object file a.o
.
Java works in a similar way: when then compiler imports a class A into class B, it looks at the class file for A and does not need the file A.java
. So all definitions and initializations for class A can be put in one source file.
Going back to C++, the reason why in C++ you have to define static data members in a separate file is more related to the C++ compilation model than to limitations imposed by the linker or other tools used by the compiler.
In C++, importing some symbols means to build their declaration as part of
the current compilation unit. This is very important, among other things,
because of the way in which templates are compiled. But this implies that you cannot / should not define any global symbols (functions, variables, methods, static data members) in an included file, otherwise these symbols could be
multiply-defined in the compiled object files.
inline static int x[] = {1, 2, 3};
. See en.cppreference.com/w/cpp/language/static#Static_data_members