Static Member Variables

In the Introduction to global variables lesson, we introduced global variables, and in the Static local variables lesson, we introduced static local variables. Both of these types of variables have static duration, meaning they are created at the start of the program and destroyed at the end. Such variables keep their values even if they go out of scope.

For example:

#include <iostream>

int generateTicketNumber()
{
    static int s_ticketNumber{ 1000 }; // static local variable
    return ++s_ticketNumber;
}

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

    return 0;
}

This program prints:

1001
1002
1003

Note that static local variable s_ticketNumber has kept its value across multiple function calls.

Class types bring two more uses for the static keyword: static member variables and static member functions. Fortunately, these uses are fairly straightforward. We'll talk about static member variables in this lesson and static member functions in the next.

Static member variables

Before we explore the static keyword as applied to member variables, first consider the following class:

#include <iostream>

struct Counter
{
    int count{ 0 };
};

int main()
{
    Counter first{};
    Counter second{};

    first.count = 5;

    std::cout << first.count << '\n';
    std::cout << second.count << '\n';

    return 0;
}

When we instantiate a class object, each object gets its own copy of all normal member variables. In this case, because we've declared two Counter class objects, we end up with two copies of count: first.count and second.count. first.count is distinct from second.count. Consequently, the program above prints:

5
0

Member variables of a class can be made static by using the static keyword. Unlike normal member variables, static member variables are shared by all objects of the class. Consider the following program, similar to the above:

#include <iostream>

struct Counter
{
    static int s_count; // declare s_count as static (initializer moved below)
};

int Counter::s_count{ 0 }; // define and initialize s_count to 0 (we'll discuss this section below)

int main()
{
    Counter first{};
    Counter second{};

    first.s_count = 5;

    std::cout << first.s_count << '\n';
    std::cout << second.s_count << '\n';
    return 0;
}

This program produces the following output:

5
5

Because s_count is a static member variable, s_count is shared between all objects of the class. Consequently, first.s_count is the same variable as second.s_count. The above program shows that the value we set using first can be accessed using second!

Static members are not associated with class objects

Although you can access static members through objects of the class (as shown with first.s_count and second.s_count in the example above), static members exist even if no objects of the class have been instantiated! This makes sense: they are created at the start of the program and destroyed at the end, so their lifetime is not bound to a class object like a normal member.

Essentially, static members are global variables that live inside the scope region of the class. There is very little difference between a static member of a class and a normal variable inside a namespace.

Key Concept
Static members are global variables that live inside the scope region of the class.

Because static member s_count exists independently of any class objects, it can be accessed directly using the class name and the scope resolution operator (in this case, Counter::s_count):

class Counter
{
public:
    static int s_count; // declare s_count as static
};

int Counter::s_count{ 0 }; // define and initialize s_count to 0 (we'll discuss this section below)

int main()
{
    // note: we're not instantiating any objects of type Counter

    Counter::s_count = 5;
    std::cout << Counter::s_count << '\n';
    return 0;
}

In the above snippet, s_count is referenced by class name Counter rather than through an object. Note that we have not even instantiated an object of type Counter, but we are still able to access and use Counter::s_count. This is the preferred method for accessing static members.

Best Practice
Access static members using the class name and the scope resolution operator (::).

Defining and initializing static member variables

When we declare a static member variable inside a class type, we're telling the compiler about the existence of a static member variable, but not actually defining it (much like a forward declaration). Because static member variables are essentially global variables, you must explicitly define (and optionally initialize) the static member outside of the class, in the global scope.

In the example above, we do so via this line:

int Counter::s_count{ 0 }; // define and initialize s_count to 0

This line serves two purposes: it instantiates the static member variable (just like a global variable) and initializes it. In this case, we're providing the initialization value 0. If no initializer is provided, static member variables are zero-initialized by default.

Note that this static member definition is not subject to access controls: you can define and initialize the value even if it's declared as private (or protected) in the class (as definitions are not considered to be a form of access).

For non-template classes, if the class is defined in a header (.h) file, the static member definition is usually placed in the associated code file for the class (e.g., Counter.cpp). Alternatively, the member can also be defined as inline and placed below the class definition in the header (this is useful for header-only libraries). If the class is defined in a source (.cpp) file, the static member definition is usually placed directly underneath the class. Do not put the static member definition in a header file (much like a global variable, if that header file gets included more than once, you'll end up with multiple definitions, which will cause a linker error).

For template classes, the (templated) static member definition is typically placed directly underneath the template class definition in the header file (this doesn't violate the ODR because such definitions are implicitly inline).

Initialization of static member variables inside the class definition

There are a few shortcuts to the above. First, when the static member is a constant integral type (which includes char and bool) or a const enum, the static member can be initialized inside the class definition:

class Settings
{
public:
    static const int s_maxConnections{ 100 }; // a static const int can be defined and initialized directly
};

In the above example, because the static member variable is a const int, no explicit definition line is needed. This shortcut is allowed because these specific const types are compile-time constants.

In the Sharing global constants across multiple files (using inline variables) lesson, we introduced inline variables, which are variables that are allowed to have multiple definitions. C++17 allows static members to be inline variables:

class Settings
{
public:
    static inline int s_maxConnections{ 100 }; // a static inline variable can be defined and initialized directly
};

Such variables can be initialized inside the class definition regardless of whether they are constant or not. This is the preferred method of defining and initializing static members.

Because constexpr members are implicitly inline (as of C++17), static constexpr members can also be initialized inside the class definition without explicit use of the inline keyword:

#include <string_view>

class Settings
{
public:
    static constexpr double s_version{ 2.5 }; // ok
    static constexpr std::string_view s_appName{ "MyApp" }; // this even works for classes that support constexpr initialization
};
Best Practice
Make your static members `inline` or `constexpr` so they can be initialized inside the class definition.

An example of static member variables

Why use static variables inside classes? One use is to assign a unique ID to every instance of the class. Here's an example:

#include <iostream>

class Widget
{
private:
    static inline int s_idGenerator{ 1 };
    int m_id{};

public:
    // grab the next value from the id generator
    Widget() : m_id{ s_idGenerator++ }
    {
    }

    int getID() const { return m_id; }
};

int main()
{
    Widget first{};
    Widget second{};
    Widget third{};

    std::cout << first.getID() << '\n';
    std::cout << second.getID() << '\n';
    std::cout << third.getID() << '\n';
    return 0;
}

This program prints:

1
2
3

Because s_idGenerator is shared by all Widget objects, when a new Widget object is created, the constructor initializes m_id with the current value of s_idGenerator and then increments the value for the next object. This guarantees that each instantiated Widget object receives a unique id (incremented in the order of creation).

Giving each object a unique ID can help when debugging, as it can be used to differentiate objects that otherwise have identical data. This is particularly true when working with arrays of data.

Static member variables are also useful when the class needs to utilize a lookup table (e.g., an array used to store a set of pre-calculated values). By making the lookup table static, only one copy exists for all objects, rather than making a copy for each object instantiated. This can save substantial amounts of memory.

Only static members may use type deduction (auto and CTAD)

A static member may use auto to deduce its type from the initializer, or Class Template Argument Deduction (CTAD) to deduce template type arguments from the initializer.

Non-static members may not use auto or CTAD.

The reasons for this distinction being made are quite complicated, but boil down to there being certain cases that can occur with non-static members that lead to ambiguity or non-intuitive results. This does not occur for static members. Thus non-static members are restricted from using these features, whereas static members are not.

#include <utility> // for std::pair<T, U>

class Configuration
{
private:
    auto m_x{ 10 };              // auto not allowed for non-static members
    std::pair m_v{ 1, 5.5 };     // CTAD not allowed for non-static members

    static inline auto s_x{ 10 };              // auto allowed for static members
    static inline std::pair s_v{ 1, 5.5 };     // CTAD allowed for static members

public:
    Configuration() {};
};

int main()
{
    Configuration config{};

    return 0;
}

Summary

Static member variables: Member variables that belong to the class rather than to individual objects. All objects of the class share the same static member variable, which exists even if no objects of the class have been instantiated.

Static duration: Static members have static duration, meaning they are created at program startup and destroyed at program shutdown. They retain their values throughout the program's lifetime.

Access methods: Static members can be accessed through class objects, but the preferred method is to access them directly using the class name and scope resolution operator (e.g., ClassName::s_member).

Definition and initialization: Static members must be defined outside the class (except for special cases). The definition typically appears in the associated .cpp file for non-template classes, or as inline in the header for template classes.

Inline static members (C++17): Static members can be declared inline, allowing them to be defined and initialized directly inside the class definition regardless of type. This is the preferred approach when available.

Constexpr static members: Static members declared constexpr are implicitly inline (as of C++17) and can be initialized inside the class definition without the explicit inline keyword.

Type deduction: Only static members may use auto or CTAD for type deduction. Non-static members cannot use these features due to potential ambiguity issues.

Practical uses: Common applications include assigning unique IDs to objects, sharing lookup tables across all instances, and maintaining class-wide state or counters.

Static member variables provide a way to share data across all objects of a class while maintaining encapsulation. They combine the benefits of global variables with the organizational structure of classes, making them useful for class-wide state and shared resources.