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. |
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.