Ch13.8: Static Data Members

中文(简体)

1. One Variable for the Whole Class

A normal data member belongs to an object: every account has its own id_. A static data member belongs to the class: there is exactly one of it, no matter how many objects exist, and it exists whether or not any object exists at all.

That is exactly what you want for a fact about the class rather than about an instance: the next identifier to hand out, how many objects were created, a shared limit, a table that all instances consult.


class account
{
	// one counter for the whole class:
	inline static ::std::size_t next_id_{1};

	// one number per account:
	::std::size_t id_{};
};

2. inline static and static constexpr

There are two spellings you need, and both are one line inside the class:

Declaration Meaning
inline static ::std::size_t next_id_{1}; One mutable variable for the class. inline makes this declaration also the definition, so nothing else has to be written anywhere — not even when the class is in a header included by many files.
static constexpr ::std::size_t iban_length{34}; A constant that belongs to the class. constexpr implies const and, inside a class, inline too. Use it where you would otherwise write a literal or a macro.

#include <fast_io.h>
#include <cstddef>
using namespace ::fast_io::io;

class account
{
public:
	account() noexcept : id_{next_id_++} // every account takes the next number
	{
	}

	::std::size_t id() const noexcept
	{
		return id_;
	}

	// One variable, shared by every account that exists.
	inline static ::std::size_t next_id_{1};

	// A constant that belongs to the class, not to any object.
	static constexpr ::std::size_t iban_length{34};

private:
	::std::size_t id_{};
};

int main()
{
	account a;
	account b;
	account c;

	println("a.id() = ", a.id(), ", b.id() = ", b.id(), ", c.id() = ", c.id());
	println("next id to hand out: ", account::next_id_);
	println("iban_length: ", account::iban_length, " (also a.iban_length = ", a.iban_length, ")");
}

a.id() = 1, b.id() = 2, c.id() = 3
next id to hand out: 4
iban_length: 34 (also a.iban_length = 34)

The constructor account() noexcept : id_{next_id_++} reads and writes the shared counter: each object takes the current value and leaves the counter one higher. The last line shows that a static member is also reachable through an object (a.iban_length) — legal, because both spellings mean the same single variable. Do not write it that way: it looks like per-object data, which is exactly the misunderstanding the keyword exists to prevent.

If you cannot write inline. Before C++17 the member had to be declared inside the class and defined in exactly one .cc file:


// account.h
class account
{
public:
	static ::std::size_t next_id_;   // declaration only
};

// account.cc
::std::size_t account::next_id_{1};  // the one definition

You will meet this in older code. inline static and static constexpr do the same job in one line and cannot get out of sync, which is why this tutorial uses them.

3. Static Data Members Are Global State

The counter in the example is honest: it is a sequence number, every user of it wants the same sequence, and every object that takes a number increments it. Use static data for exactly this kind of fact about the class.

Be careful with everything else. A static data member is a global variable that happens to be spelled account::something. It is visible to every line of code in the program, it lives for the whole run, and it is shared by every thread. Those are the properties that make globals hard to reason about, and the class scope does not remove them:

Good use Bad use
An identifier or serial number generator for objects of the class. A cache that makes a const member function secretly non-const in effect.
A compile-time constant describing the class (iban_length). Configuration that a program may want two different values of in two different parts of the code.
Optionally, counters and tables that really are per-class and are documented as such. Anything holding ownership of resources, or anything that the correctness of unrelated code depends on.
A mutable static data member is shared by every thread. If two threads can create accounts at the same time, then next_id_++ is a data race, and the program has undefined behaviour. Making it ::std::atomic<::std::size_t> makes the increment safe; it does not make a more complicated shared scheme correct. If your design has a static member that many threads write, treat that as a design problem first.
Prefer a member of the class over a global variable. Even when you do need one shared value, putting it inside the class that uses it is strictly better than a free global: the name says who owns it, the access level says who may touch it, and the definition sits next to the code that maintains it.