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();
}
x is now 1
x is now 2
x is now 3
x is now 4
x is now 5

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.

https://isocpp.org/wiki/faq/ctors#static-init-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 a static 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 and static global objects in a header file. Since such entities have internal linkage, there are no reasons for other TUs to see them.