09 - static fields

As stated in a previous chapter, static modifies object's storage duration.

A class can contain static members: variables (fields) and functions (methods). static fields, unlike other static objects do not have reduced linkage. They can still be used in other TUs.

This lesson uses structures in examples to simplify and shorten code.

Said differently, static fields are just global objects that are placed inside class namespace.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// foo.hpp
struct foo
{
	foo(int x): x(x) {}

	int x = 0;
	static int s; // static member
};

// foo.cpp
int foo::s = 1; // initialization of static member

// main.cpp
#include <iostream>
#include <memory>

int main()
{
	foo f1(10);
	foo f2(20);

	// static members are shared across all instances
	std::cout << "f1.s = " << f1.s << "\n";
	std::cout << "f2.s = " << f2.s << "\n";

	++f1.s;

	std::cout << "f1.s = " << f1.s << "\n";
	std::cout << "f2.s = " << f2.s << "\n";

	// it's actually the same object
	std::cout << "address of f1.s = " << std::addressof(f1.s) << "\n";
	std::cout << "address of f2.s = " << std::addressof(f2.s) << "\n";

	// static members are not a part of the object
	// the class has 2 ints but only 1 contributes to the size
	std::cout << "sizeof(int) = " << sizeof(int) << "\n";
	std::cout << "sizeof(foo) = " << sizeof(foo) << "\n";

	// you actually don't need any object to access static members
	++foo::s;
	std::cout << "foo::s = " << foo::s << "\n";
}
f1.s = 1
f2.s = 1
f1.s = 2
f2.s = 2
address of f1.s = 0x600eb8
address of f2.s = 0x600eb8
sizeof(int) = 4
sizeof(foo) = 4
foo::s = 3

Because static member objects are global it's recommended to refer to them through the class name, not specific objects. The syntax which uses . helps in some situations (e.g. templates which don't know on what type they operate) but can be misleading.

In practice

Obviously global state is bad so static fields should be avoided. A typical example where they can be used is generation of unique IDs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>

class user
{
public:
	user(/* params... */)
	: id(++id_counter) {}

	int get_id() const { return id; }

	// other member functions...

private:
	int id;
	// other fields (e.g. name, password, ...)

	static int id_counter;
};

// even private static members can be initialized this way
int user::id_counter = 0;

int main()
{
	user u1; // gets id == 1
	user u2; // gets id == 2

	{
		user u3; // gets id == 3
	} // u3 dies here; id == 3 will not be reused

	user u4; // gets id == 4
	std::cout << u4.get_id() << "\n";
}

Here each user object will get a unique ID. The id member could be const, but there are good reasons to not use the keyword even if the field never changes (the main one being prevention of assignment of objects of this class).

Static initialization order fiasco

The same fiasco can happen with static members:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct bar {
	static int x;
};

struct baz {
	static int y;
};

// one translation unit
int bar::x = 1;

// other translation unit
int baz::y = bar::x;

The solution is the same as with other static objects - bundle them together into one type and move static from these objects to the instance of this type:

1
2
3
4
5
6
7
8
9
10
11
12
struct barz {
	barz(int val): x(val), y(val) {}

	int x;
	int y;
};

struct foo {
	static barz b;
};

barz foo::b(1);

Syntax sugar

There are ways to avoid the need of initialization outside the class definition:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// since C++11
// if the static member is const, it can be initialized in the class definition
struct foo
{
	static const int s = 1; // static const is implicitly inline
};

// since C++11
// if the static member is constexpr, it has to be initialized in the class definition
struct bar
{
	static constexpr int s = 1; // constexpr initialization implies inline
};

// since C++17
// static members can be marked inline and initialized in the class definition
struct baz
{
	inline static int s = 1;
};

The C++17 improvement is most significant, because it completely removes the need to write initialization of static fields outside class definition. Apart from shorter code, it eliminates a common mistake when the initialization was simply forgotten.

Summary

  • static fields are affected by access specifiers (except initialization statement).

  • static fields are not associated with any object. They exist even if no objects of the class have been created.

  • static fields can not be mutable.

  • static fields can also be declared thread_local.

  • static fields are initialized before main starts executing.

Does it mean that through constructor of a static object I can inject a function to be run before main?

Yes. Obviously this should not be abused. Order of intialization of objects with static storage duration is undefined but C++ guarantees that standard library objects are initialized first - this means that you can use std::cout before main starts executing.