What Are Static Member Variables?

A static member variable is a variable that belongs to the class itself rather than to any object of that class. However many objects you create, and even if you create none at all, exactly one copy of it exists for the whole program.

You have already met the static keyword on a local variable, where it changes the variable's duration: instead of being created and destroyed on every call, the variable is created once at program startup and destroyed at program shutdown, keeping its value in between.

#include <iostream>

int stampJobNumber()
{
    static int s_lastJob{ 4100 }; // static local variable
    return s_lastJob++;
}

int main()
{
    std::cout << stampJobNumber() << '\n';
    std::cout << stampJobNumber() << '\n';
    std::cout << stampJobNumber() << '\n';

    return 0;
}

Output:

4100
4101
4102

Applied to a member variable, static does the same thing to duration, and adds one more consequence: the member stops being per-object and becomes per-class. This lesson is about that second effect. The next lesson covers the other class-related use of the keyword, static member functions.

One Copy Per Class, Not Per Object

Start with an ordinary member. Every object gets its own:

#include <iostream>

struct Workbench
{
    int openJobs{ 0 };
};

int main()
{
    Workbench window{};
    Workbench corner{};

    window.openJobs = 3;

    std::cout << "window bench: " << window.openJobs << '\n';
    std::cout << "corner bench: " << corner.openJobs << '\n';

    return 0;
}

Output:

window bench: 3
corner bench: 0

Two objects, two openJobs variables. window.openJobs and corner.openJobs are separate storage, so writing one leaves the other untouched.

Now mark the member static and nothing else about the program changes:

#include <iostream>

struct Workbench
{
    static inline int s_openJobs{ 0 }; // one variable for the whole class
};

int main()
{
    Workbench window{};
    Workbench corner{};

    window.s_openJobs = 3;

    std::cout << "window bench: " << window.s_openJobs << '\n';
    std::cout << "corner bench: " << corner.s_openJobs << '\n';

    return 0;
}

Output:

window bench: 3
corner bench: 3

There is now one s_openJobs in the entire program, and both objects name it. window.s_openJobs and corner.s_openJobs are not two variables that happen to agree, they are the same variable spelled two ways. A value written through one object is immediately visible through every other.

The s_ prefix is a naming convention, not a language rule, but it is worth following: at the point of use, s_openJobs warns the reader that an assignment here is going to be seen by every other object of the class.

Non-static member Static member
Copies in memory One per object Exactly one, per class
Created When an object is constructed At program startup
Destroyed When that object is destroyed At program shutdown
Requires an object to use Yes No
Usually written as object.member ClassName::member

The Object Is Optional

Because a static member is not part of any object, it does not need one. It is already there before main() begins, whether or not the class is ever instantiated:

#include <iostream>

class Workshop
{
public:
    static inline int s_powerHours{ 0 };
};

int main()
{
    // no Workshop object is created anywhere in this program

    Workshop::s_powerHours = 6;

    std::cout << Workshop::s_powerHours << " hours on the mains\n";

    return 0;
}

Output:

6 hours on the mains

The class name plus the scope resolution operator is all it takes. This is the honest way to write it, because it says out loud that the variable belongs to Workshop and not to some particular workshop.

Key Concept
A static member variable is a global variable that happens to live inside the scope region of a class. Apart from the access controls a class provides, there is almost no difference between it and a variable declared in a namespace.
Best Practice
Access static members through the class name and the scope resolution operator (ClassName::s_member), not through an object.

Where the Storage Actually Lives

Writing static int s_powerHours; inside a class does not create the variable. It only tells the compiler that a variable by that name exists somewhere, much as a forward declaration does for a function. Build a program with only the declaration and the compiler is satisfied, but the linker is not.

This program is broken:

#include <iostream>

class Workshop
{
public:
    static int s_powerHours; // declares the member, but does not create it
};

int main()
{
    std::cout << Workshop::s_powerHours << '\n';

    return 0;
}
/usr/bin/ld: in function `main':
s.cpp:(.text.startup+0x4): undefined reference to `Workshop::s_powerHours'
/usr/bin/ld: s.cpp:(.text.startup+0xc): undefined reference to `Workshop::s_powerHours'
collect2: error: ld returned 1 exit status

The fix is to define the variable at global scope, outside the class, the same way you would define a global:

#include <iostream>

class Workshop
{
public:
    static int s_powerHours; // declaration inside the class
};

int Workshop::s_powerHours{ 6 }; // definition at global scope

int main()
{
    std::cout << Workshop::s_powerHours << " hours on the mains\n";

    return 0;
}

Output:

6 hours on the mains

That one line does two jobs: it creates the variable, and it initializes it. Leave the initializer off and the variable is zero-initialized, again matching how globals behave.

Two details about that definition line are worth knowing:

  • It ignores access specifiers. A private static member is still defined this way from outside the class. Defining something is not the same as accessing it, so the compiler permits it.
  • It belongs in exactly one file. For a non-template class declared in a header, put the definition in the matching source file, such as Workshop.cpp. For a template class, put it in the header directly below the class, where it is implicitly inline and therefore safe to see more than once.
Warning
Never put a plain out-of-class static member definition in a header. If that header is included by two source files, the program ends up with two definitions of the same variable and the link fails, exactly as it would for a global variable defined in a header.

Choosing How to Initialize

The out-of-class definition is the fallback, not the goal. Several kinds of static member can be initialized right where they are declared, which keeps the value next to the name and removes the separate definition entirely.

Declared as Where it is initialized Restriction
static int s_limit; Out of class, in one source file None, but it is the most work
static const int s_limit{ 12 }; In the class Const integral types and const enums only
static inline T s_limit{ ... }; In the class C++17 or later, any type
static constexpr T s_limit{ ... }; In the class Value must be a compile-time constant

The inline row is the one that changed everything. Inline variables are permitted to have more than one definition, so C++17 allowing static members to be inline means any static member, const or not, can carry its initializer inside the class. And because constexpr implies inline for static members, a constexpr member gets the same treatment without you writing the keyword.

#include <iostream>
#include <string_view>

class Tolerance
{
public:
    static const int s_maxBeatError{ 12 };             // const integral: allowed in class
    static inline double s_rateWindow{ 0.5 };          // inline: allowed for any type
    static constexpr double s_amplitudeFloor{ 220.0 }; // constexpr is implicitly inline
    static constexpr std::string_view s_gradeLabel{ "chronometer" };
};

int main()
{
    std::cout << Tolerance::s_gradeLabel << '\n';
    std::cout << Tolerance::s_maxBeatError << " seconds per day\n";
    std::cout << Tolerance::s_rateWindow << " seconds of drift allowed\n";
    std::cout << Tolerance::s_amplitudeFloor << " degrees of amplitude\n";

    return 0;
}

Output:

chronometer
12 seconds per day
0.5 seconds of drift allowed
220 degrees of amplitude

Note that s_gradeLabel is not an integral type at all. std::string_view supports constant initialization, so it works as a constexpr member, and the older const-integral shortcut would never have allowed it.

Best Practice
Declare static members inline or constexpr so they can be initialized inside the class definition. Reach for the out-of-class definition only when something prevents that.

Stamping Every Object With Its Own Number

The most common reason to want per-class storage is to hand out something that must not repeat. A single shared counter, read and advanced by each constructor, gives every object a number no other object has:

#include <iostream>

class Movement
{
private:
    static inline int s_nextDocket{ 501 }; // one counter for the whole class
    int m_docket{};                        // one number per object

public:
    Movement() : m_docket{ s_nextDocket++ }
    {
    }

    int docket() const { return m_docket; }
};

int main()
{
    Movement calibre{};
    Movement pocketWatch{};
    Movement wallClock{};

    std::cout << calibre.docket() << '\n';
    std::cout << pocketWatch.docket() << '\n';
    std::cout << wallClock.docket() << '\n';

    return 0;
}

Output:

501
502
503

The class combines both kinds of member deliberately. s_nextDocket is static, so all three constructors see the same counter; m_docket is not, so each object keeps the value it was given. Every construction copies the counter into the new object and then increments it, which is what makes the numbers unique and puts them in creation order.

Numbers like these are mostly a debugging aid. When you are staring at several objects whose data looks identical, a docket number is often the only thing that tells you which one you are looking at.

One Table, Every Object Reads It

The other classic use is shared read-only data. A lookup table stored as an ordinary member would be duplicated into every object, wasting memory to store the same numbers repeatedly. Made static, it exists once:

#include <array>
#include <cstddef>
#include <iostream>

class Escapement
{
private:
    static constexpr std::array<int, 5> s_beatRates{ 18000, 19800, 21600, 25200, 28800 };
    std::size_t m_grade{};

public:
    explicit Escapement(std::size_t grade) : m_grade{ grade }
    {
    }

    int beatsPerHour() const { return s_beatRates[m_grade]; }
};

int main()
{
    Escapement swissLever{ 2 };
    Escapement highBeat{ 4 };

    std::cout << swissLever.beatsPerHour() << '\n';
    std::cout << highBeat.beatsPerHour() << '\n';

    return 0;
}

Output:

21600
28800

Each Escapement object carries one std::size_t. The table of rates is shared, and marking it constexpr means no object can modify it.

Warning
A public non-const static member is shared mutable state that any code in the program can reach and change, with all the hazards that gives a non-const global. Keep such members private, and let member functions control how they are read and written.

Type Deduction Is a Static-Only Privilege

Static members are allowed one thing that ordinary members are not: they may deduce their own type from their initializer, either with auto or through class template argument deduction.

This does not compile:

#include <array>

class Caliper
{
private:
    auto m_reading{ 3 };             // auto is not allowed for a non-static member
    std::array m_offsets{ 1, 2, 3 }; // CTAD is not allowed for a non-static member
};

int main()
{
    return 0;
}
s.cpp:6:5: error: non-static data member declared with placeholder 'auto'
    6 |     auto m_reading{ 3 };             // auto is not allowed for a non-static member
      |     ^~~~
s.cpp:7:5: error: invalid use of template-name 'std::array' without an argument list
    7 |     std::array m_offsets{ 1, 2, 3 }; // CTAD is not allowed for a non-static member
      |     ^~~

Add static to both and the same declarations are fine:

#include <array>
#include <iostream>

class Caliper
{
public:
    static inline auto s_reading{ 3 };             // auto deduces int
    static inline std::array s_offsets{ 1, 2, 3 }; // CTAD deduces std::array<int, 3>
};

int main()
{
    std::cout << Caliper::s_reading << '\n';
    std::cout << Caliper::s_offsets.size() << '\n';

    return 0;
}

Output:

3
3

The reasoning behind the restriction is intricate, but the short version is that a non-static member's initializer can be replaced by a constructor's member initializer list, so the type a reader deduces from the declaration may not be the type the object ends up with. A static member has a single initializer that no constructor can override, so there is nothing ambiguous to deduce from.

Summary

One per class: a static member variable exists once for the entire class, and every object of that class shares it. Writing to it through one object changes what every other object sees.

Static duration: static members are created at program startup and destroyed at program shutdown, so they exist even when no object of the class has ever been instantiated.

Access: reach them through the class name and the scope resolution operator, as in ClassName::s_member. Access through an object compiles but implies a relationship that is not there.

Declaration versus definition: the static line inside the class only declares the member. A plain static member must be defined at global scope outside the class, normally in the class's source file, or the link fails with an undefined reference.

In-class initialization: const integral members have always been allowed to initialize in the class. Since C++17, inline extends that to any type, and constexpr members are implicitly inline and get it for free. Prefer these forms.

Uses: handing out unique per-object numbers from a shared counter, sharing a lookup table instead of copying it into every object, and holding class-wide constants.

Type deduction: only static members may use auto or CTAD. Non-static members must state their type.