01 - static and extern
static
is a pretty overloaded keyword in C++. It's meaning depends on the context:
when applied to a function-local objects, modifies their storage duration
when applied to a global object or a function definitions, modifies their linkage
when applied to data or function member of a class, modifies its semantics and both storage duration and linkage
This lesson will explain first 2 contexts.
Function-local static
objects
Objects are being destroyed when their scope ends - basically enclosing }
. But this is not the case in the following program:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <iostream> void run() { static int x = 0; std::cout << "x is now " << ++x << "\n"; } int main() { for (int i = 0; i < 5; ++i) run(); } |
The variable acts as if it lived beyond the function body - and in fact, this is what happens for objects with static storage duration. This brings us to the topic of this lesson.
Storage duration
C++ specifies 4 kinds of object lifetime.
automatic storage duration - basically everything you have used so far. Objects are limited by enclosing braces and die when they go out of scope. This is the default storage duration for everything.
-
static storage duration - object is created when the program starts or on first use and destroyed when the program ends. This applies to:
global objects
objects declared with
static
objects declared with
extern
(all external objects are global)
thread storage duration - similarly to static, but with the difference that the object is created when the thread launches and destroyed when the thread finishes. Each thread will have its own (independent) instance of the object. Aplies to objects declared with
thread_local
.dynamic storage duration - object is (allocated and/or created) and (deallocated and/or destroyed) explicitly. Related with resource management. Opens many powerful features but often comes with a cost. Done through
new
,delete
, placement-new and pseudo-destructor calls.
Function-local static
objects behave differently from global static
objects and functions. Let's move to another example.
static
functions
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// main.cpp #include "greet.hpp" #include <iostream> int main() { greet(); } // greet.hpp #pragma once static void greet(); // greet.cpp #include "greet.hpp" #include <iostream> static void greet() { std::cout << "hello, world\n"; } |
When trying to build the program, we get a surprising output. First, a warning is emmited. Then the build fails but at the linking stage, not compilation:
In file included from main.cpp:1: greet.hpp:3:13: warning: ‘void greet()’ used but never defined static void greet(); ^~~~~ /tmp/ccmydgVK.o: In function `main': main.cpp:(.text+0x5): undefined reference to `greet()' collect2: error: ld returned 1 exit status
So what has actually gone wrong? static
, when applied to free (AKA non-member) functions changes their linkage. First, the compiler warns that a static
function has been declared but not defined. This isn't usually a problem - definitions of many things can be placed in another translation unit. But in this case, it is a problem. The keyword has informed the compiler that the function should have internal linkage instead of default external linkage for functions, which led to the linker error.
Linkage
cppreference:
A name that denotes object, reference, function, type, template, namespace, or value, may have linkage. If a name has linkage, it refers to the same entity as the same name introduced by a declaration in another scope. If a variable, function, or another entity with the same name is declared in several scopes, but does not have sufficient linkage, then several instances of the entity are generated.
Said differently, linkage affects how an entity is seen by other code. Entities with insufficient linkage will not be visible to other entities. The simplest example are variables defined locally in different functions - they just don't see each other and thus can have the same name while being separate objects.
There are 3 kinds of linkage:
-
no linkage - name can be referred only from the scope it is declared. Applies to:
all objects declared without
extern
everything else declared locally (inside a function)
-
internal linkage - name can be referred from all scopes in current translation unit. Applies to:
objects and functions declared
static
members of anonymous unions
members of anonymous namespaces
-
external linkage - name can be referred from all scopes in any translation unit. Applies to:
objects declared
extern
basically everything else which is not
static
Combing to the example,
The compiler emmited a warning that the function is used but not defined because it knew that due to
static
the function could not be defined in a different TU.The linker could not find the definition for the function because the compiled function had internal linkage. It was not accessible outside its own TU.
The example showcased a function but a global static
object would have a similar problem. But instead of ending in a linker error, it would create a different object with the same name for each TU. This is because static
objects (unlike functions) can not be just declared, the statement already forms a definition. The exact same problem would reproduce if the header used extern
and source static
.
extern
Functions by default have external linkage. extern
can be added to them, but is not necessary.
But the same is not true for global objects. If you write something like int x;
in a header file, it will create a different object for each TU that includes such statement. This is because for objects (unlike for functions), there is no immediate way to differentiate a declaration from a definition. Function declarations are easily tell apart from definitions due to the lack of body. But for objects, you need to write extern int x;
in order to change the meaning from definition to just a declaration. Such declaration can then be included as many times as needed and exactly one TU should define it. That's basically how you create a global object that is shared across TUs.
1 2 3 4 5 |
// header extern int x; // exactly one source file int x = 1; |
extern "C"
Specific entities (function types, functions and global objects) with external linkage also have language linkage. This feature allows to link code defined in different programming languages and is the standard way of C/C++ interoperatibility at the binary (compiled code) level.
The standard mandates support of only extern "C"
and extern "C++"
but an implementation is allowed to support more.
For a detailed explanation how to use language linkage, read TODO link.
You may also be interested in:
Static initialization order fiasco
Initialization of global objects across TUs has undefined order. If one's object initialization depends on another object that is defined in a different TU, the behavior is undefined.
Workaround: since these objects are already related, put them in a struct
. Now you can write a function (constructor or a free function) that will have well-defined order.
In practice
At this point you might be wondering - what's the point of static
functions and objects?
They serve multiple purposes:
Less entities with large linkage means less work for the linker. This speeds up the build process.
Code clarity: when seeing a declaration or a definition of a
static
function or astatic
global object, you are sure it's not being used in other TUs.The same function name can be reused for multiple functions in different TUs as the
static
effectively reduces their visibility to a single TU.If the project is built as a library (static or dynamic), the resulting binary object size is smaller as entities without external linkage do not have to be reported there.
The reality is a bit more complicated than what C++ specification covers, although the same concept applies. It's just more detailed because modern systems support both static linking (merging compiled code into an executable) and dynamic linking (loading compiled code from separate files - usually named *.so
(shared object) on POSIX systems and *.dll
(dynamic link library) on Windows).
GCC, when speaking of linkage on implementation-level calls it visibility. This article on GCC wiki explains benefits of using detailed support for symbol visibility. Macros like the ones presented in the article have become the conventional way of writing libraries. In C++20 the situation has been further improved by introducing modules, a C++ standard way of specifying which code is public (for others to use) and which code is private (implementation detail).
Article clarifications
DSO means the same thing as dynamic library
C++ doesn't specify beyond translation unit. In reality we can also speak about library units.
-fvisibility=hidden
and__attribute__ ((visibility("hidden")))
allow to mark code with external linkage for working across TUs but not across library units.
Summary
Storage duration (lifetime) and linkage (visibility) are 2 independent properties of each object.
Cheatsheet:
objects with automatic and dynamic storage duration always have no linkage
-
objects with static storage duration:
no linkage - function-local
static
internal linkage - global
static
external linkage - global
extern
-
objects with thread storage duration:
no linkage -
thread_local
internal linkage -
static thread_local
external linkage -
extern thread_local
Can you guess storage duration of std::cout
?
answer
static
Can you guess linkage of std::cout
?
answer
external
Recommendation
Avoid function-local
static
objects. They are essentially global variables, just with reduced accessibility.Never write declarations or definitions of
static
functions andstatic
global objects in a header file. Since such entities have internal linkage, there are no reasons for other TUs to see them.