In C++ (not C), a global const array uses internal linkage for optimization
"Optimization" is perhaps not the right word. Default internal linkage
for const
file-scope objects allows us to define const
objects in
header files without having to prefix static
, or enclose them in an anonymous
namespace, to head off multiple-definition linkage errors. This is convenient
and intuitive. Optimizations may accrue, or not, depending on this and that.
"File-scope" is certainly a better word that "global" in this connection. You'll
see why in a while.
And there's nothing special about arrays on this score. All const
file-scope
objects have internal linkage by default, in C++.
So maybe your question can be sharpened up as: Does C++ guarantee that distinct file-scope const
objects in different translation units that have the same name, type and byte-wise
value will be merged to a single copy in a program that they are linked in?
No it doesn't. On the contrary, the C++ Standard probibits distinct objects in a
program (other than object and sub-object) from having the same address:
C++11 [intro.object], para 6
Unless an object is a bit-field or a base class subobject of zero size, the address
of that object is the address of the first byte it occupies. Two objects that are
not bit-fields may have the same address if one is a subobject of the other, or
if at least one is a base class subobject of zero size and they are of different types;
otherwise, they shall have distinct addresses4.
(emphasis mine). Later standards have words to the same effect.
There is a crevice of wriggle-room provided by that footnote [4]:
4) Under the “as-if” rule an implementation is allowed to store two objects at
the same machine address or not store an
object at all if the program cannot observe the difference.
But if distinct objects are distinguishable in a program, then they must not
have the same address - which they would do, were they merged.
And even if the Standard did not make this stipulation, the merging of identical
file-scope const
objects from different translation units would be unfeasible anyway.
Consider:
array.h
#ifndef ARRAY_H
#define ARRAY_H
const int Arr[10]={1,6,3,5,5,6,8,8,9,20};
#endif
foo.cpp
#include "array.h"
#include <iostream>
void foo()
{
std::cout << "Address of `Arr` in `foo.cpp` = " << Arr << std::endl;
}
bar.cpp
#include "array.h"
#include <iostream>
void bar()
{
std::cout << "Address of `Arr` in `bar.cpp` = " << Arr << std::endl;
}
main.cpp
extern void foo();
extern void bar();
int main()
{
foo();
bar();
return 0;
}
Compile all those source files to object files:
g++ -Wall -c foo.cpp bar.cpp main.cpp
The compiler encountered a
const int Arr[10]={1,6,3,5,5,6,8,8,9,20};
in compiling foo.cpp
to foo.o
and accordingly defined an object
in foo.o
:
$ readelf -s foo.o | grep Arr
6: 0000000000000000 40 OBJECT LOCAL DEFAULT 5 _ZL3Arr
_ZL3Arr
is the name-mangling of file-scope symbol Arr
:
$ c++filt _ZL3Arr
Arr
40
is the size of the object in bytes, which is right for 10 4-byte integers.
The object is LOCAL
:
LOCAL
= internal linkage = invisible to the linker
GLOBAL
= external linkage = visible to the linker
(That's why "file-scope" was a better word than "global").
The object is defined in the linkage section with index 5
in foo.o
. readelf
can also tell us what linkage
section that is:
$ readelf -t foo.o
There are 15 section headers, starting at offset 0x7e0:
Section Headers:
[Nr] Name
Type Address Offset Link
Size EntSize Info Align
Flags
[ 0]
NULL NULL 0000000000000000 0000000000000000 0
0000000000000000 0000000000000000 0 0
[0000000000000000]:
...
...
[ 5] .rodata
PROGBITS PROGBITS 0000000000000000 00000000000000e0 0
0000000000000053 0000000000000000 0 32
[0000000000000002]: ALLOC
...
...
Section 5 is .rodata
, that is readonly data. Arr
has been put in read-only data
because it's const
.
For the same reasons, the same things are all true of bar.o
:
$ readelf -s bar.o | grep Arr
6: 0000000000000000 40 OBJECT LOCAL DEFAULT 5 _ZL3Arr
So each of foo.o
and bar.o
contains its own 40-byte object _ZL3Arr
that is LOCAL
and read-only. Compilation is all done and
we haven't got a program yet. So if the _ZL3Arr
in foo.o
and the _ZL3Arr
in bar.o
were going to be merged in the program, they'd have to be merged by the linker.
And even if we wanted it to, or C++ allowed it to, the linker can't do that, because
the linker can't see them!
Let's do the linkage and ask for the linker's mapfile:
$ g++ -o prog main.o foo.o bar.o -Wl,-Map=prog.map
Mapfile hits for the really global ( = GLOBAL
) symbols:
$ grep -Po 'foo' prog.map | wc -w
12
$ grep -Po 'bar' prog.map | wc -w
10
$ grep -Po 'main' prog.map | wc -w
8
Mapfile hits for Arr
:
$ grep -Po 'Arr' prog.map | wc -w
0
But readelf
can see local symbols, and now we've got a program:
$ readelf -s prog | grep Arr
36: 0000000000000b20 40 OBJECT LOCAL DEFAULT 16 _ZL3Arr
42: 0000000000000b80 40 OBJECT LOCAL DEFAULT 16 _ZL3Arr
So prog
contains two 40 byte LOCAL
objects by the name of _ZL3Arr
,
both in linkage section 16 of the program, which is...
$ readelf -t prog
There are 29 section headers, starting at offset 0x2ce8:
Section Headers:
[Nr] Name
Type Address Offset Link
Size EntSize Info Align
Flags
...
...
[16] .rodata
PROGBITS PROGBITS 0000000000000b00 0000000000000b00 0
00000000000000d1 0000000000000000 0 32
[0000000000000002]: ALLOC
...
...
once again, the read-only data.
readelf
also said that the first of those _ZL3Arr
s is at program offset 0xb20
; the second
is at 0xb80
1. So when we finally run the program we should be pleased,
but not surprised, to see that:
$ ./prog
Address of `Arr` in `foo.cpp` = 0x55edf0dd6b20
Address of `Arr` in `bar.cpp` = 0x55edf0dd6b80
the local Arr
referenced by foo()
and the one referenced by bar()
remain
0x60 bytes apart, respectively 0xb20 and 0xb80 bytes from the start of the program in memory.
Evidently you would prefer to have just one Arr
, not two, in the program. To
achieve that you have to compile:
const int Arr[10]={1,6,3,5,5,6,8,8,9,20};
in just one object file, with external linkage, so the linker can see it there,
and refer to that one object in all other object files. Like so:
array.h (revised)
#ifndef ARRAY_H
#define ARRAY_H
extern const int Arr[10];
#endif
array.cpp
#include "array.h"
const int Arr[10]={1,6,3,5,5,6,8,8,9,20};
Other files as before. In array.h
we are expressly declaring that Arr
has external linkage, and that declaration in seen and honoured by the compiler in array.cpp
.
Compile and link:
$ g++ -Wall -c main.cpp foo.cpp bar.cpp array.cpp
$ g++ -o prog main.o foo.o bar.o array.o
What's the Arr
count in the program now?
$ readelf -s prog | grep 'Arr'
60: 0000000000000b80 40 OBJECT GLOBAL DEFAULT 16 Arr
One. Still in the read-only data. But now GLOBAL
. And prog
agrees
that there is only one Arr
:
$ ./prog
Address of `Arr` in `foo.cpp` = 0x562a4fb7bb80
Address of `Arr` in `bar.cpp` = 0x562a4fb7bb80
[1] Some close readers might wonder why we see offsets rather than absolute addresses
here. It's because my Ubuntu 17.10 toolchain make PIE executables by default.