Static Members
Static data members and static member functions belong to the class type itself, not to any instance, and are shared across all objects of that class.
Static Memberssince C++98A static member of a class β whether a data member or a member function β belongs to the class itself rather than to any individual object instance, meaning there is exactly one copy shared across all objects and the member can be accessed without an object.
Overview
Static members come in two forms: static data members and static member functions. Both are declared inside the class body using the static keyword, but neither is tied to a particular instance.
Static data members have static storage duration. Regardless of how many objects of the class exist β even zero β there is exactly one copy of each static data member in the program. They are initialized before main is entered and destroyed after it returns.
Static member functions have no implicit this parameter. They can be called through the class name without an object, and they can only directly access other static members of the class (they have no handle to any instance's non-static data).
Access control (public, protected, private) applies to static members exactly as it does to non-static members.
Syntax
class Foo {
public:
static int counter; // declaration of static data member
static void reset(); // declaration of static member function
static constexpr int kMax = 100; // C++11: constexpr integral constant, defined inline
};
// Out-of-class definition required (pre-C++17) for non-constexpr static data members
int Foo::counter = 0;
void Foo::reset() {
counter = 0;
}Since C++17, you can define a static data member inline inside the class body using the inline specifier, eliminating the need for a separate out-of-class definition:
class Bar {
public:
inline static int counter = 0; // C++17: definition lives here
};Prior to C++17, only static data members of const integral or enumeration type could be initialized inside the class body (as a convenience for compile-time constants), but they still required an out-of-class definition if odr-used:
class Baz {
static const int kBufSize = 256; // C++98/03: in-class initialization for const integral types
};
const int Baz::kBufSize; // Still needed if odr-used before C++17Examples
Instance counting
A classic use of static data members is tracking how many live instances of a class exist:
#include <iostream>
class Connection {
public:
Connection() { ++active_count_; }
~Connection() { --active_count_; }
static int active_count() { return active_count_; }
private:
inline static int active_count_ = 0; // C++17
};
int main() {
std::cout << Connection::active_count() << '\n'; // 0
Connection c1, c2;
std::cout << Connection::active_count() << '\n'; // 2
{
Connection c3;
std::cout << Connection::active_count() << '\n'; // 3
}
std::cout << Connection::active_count() << '\n'; // 2
}Static factory and class-level utilities
Static member functions are the right tool when an operation is conceptually part of a class but does not need instance data:
#include <string>
#include <optional>
class Token {
public:
enum class Kind { Identifier, Number, Symbol };
Token(std::string value, Kind kind) : value_(std::move(value)), kind_(kind) {}
static std::optional<Token> parse(std::string_view src); // C++17: string_view, optional
Kind kind() const { return kind_; }
const std::string& value() const { return value_; }
private:
std::string value_;
Kind kind_;
};
std::optional<Token> Token::parse(std::string_view src) {
if (src.empty()) return std::nullopt;
if (std::isdigit(static_cast<unsigned char>(src[0])))
return Token{std::string(src), Kind::Number};
return Token{std::string(src), Kind::Identifier};
}Static members in class templates
Each distinct template specialization gets its own copy of every static member:
template <typename T>
struct Registry {
inline static int instance_count = 0; // C++17
Registry() { ++instance_count; }
~Registry() { --instance_count; }
};
// Registry<int>::instance_count and Registry<double>::instance_count are independent variablesCalling static member functions
Static member functions can be called through the class name, through an object, or through a pointer β all three are equivalent:
struct Logger {
static void log(const char* msg);
};
Logger l;
Logger* p = &l;
Logger::log("via class name"); // preferred
l.log("via object"); // legal but misleading
p->log("via pointer"); // also legalThe object/pointer forms are legal because the compiler simply ignores the object β they produce identical code.
Best Practices
Use inline static (C++17) for data members. Defining static data members in the class body removes the fragile split between declaration and out-of-class definition, eliminates ODR violations when headers are included in multiple translation units, and co-locates the initializer with the declaration.
Prefer static constexpr for compile-time constants. Since C++11, static constexpr data members are implicitly inline (since C++17) and usable in constant expressions without needing a separate definition. This supersedes the old const integral trick.
struct Limits {
static constexpr std::size_t kMaxSize = 1024; // C++11, implicitly inline in C++17
};Use static member functions for class-scoped utilities that need access to private types or implementation details, but do not need instance state. They are preferable to free functions when the operation is intrinsically about the class (e.g., factory methods, type-level queries) and needs access to private members.
Avoid static mutable data that is not explicitly guarded. Static data members with mutable state are shared program-wide, which creates the same threading and coupling problems as global variables. If a static member must be mutated from multiple threads, protect it with a std::mutex or use std::atomic.
Common Pitfalls
Forgetting the out-of-class definition (pre-C++17)
The most common error before C++17 is declaring a static data member in the class but omitting its definition. The linker will report an undefined symbol:
// header.h
class Foo {
static int count; // declaration only
};
// Without this in exactly one .cpp, linking fails:
// int Foo::count = 0;The fix for new code is inline static int count = 0; inside the class body (C++17+).
Static initialization order fiasco
The order in which static data members in different translation units are initialized is unspecified. Relying on one TU's static being initialized before another TU's can produce undefined behaviour:
// a.cpp
int A::value = 42;
// b.cpp β may run before a.cpp initializes A::value
int B::derived = A::value * 2; // undefined if A::value not yet initializedThe standard fix is the construct-on-first-use idiom: wrap the value in a static local inside a static member function, leveraging the guarantee that function-local statics are initialized on first call (since C++11, this initialization is also thread-safe):
class Config {
public:
static Config& instance() {
static Config cfg; // C++11: thread-safe initialization
return cfg;
}
private:
Config() = default;
};Local classes cannot have static data members
A class defined inside a function body cannot have static data members β there is no valid scope in which to write the out-of-class definition:
void f() {
class Local {
// static int x; // ill-formed
};
}If you need class-scoped shared state inside a function, use a function-local static variable outside the class, or refactor to a named class at namespace scope.
operator new and operator delete are implicitly static
Class-specific operator new and operator delete overloads are always static member functions, even when declared without the static keyword. This is mandated by the standard: they operate on raw memory before any object exists (or after it is destroyed), so they cannot have a meaningful this.
Inline Static Members (C++17)
inline static moves the definition into the class body, ending the two-file dance. Before C++17, every non-constexpr static data member had to be defined in exactly one translation unit; the class body held only a declaration. inline collapses declaration and definition into one place, and the linker merges all copies from different TUs into one.
// C++17
class Widget {
public:
inline static int instance_count = 0; // definition β no .cpp entry needed
inline static constexpr int kVersion = 3; // compile-time constant, also self-contained
};The pre-C++17 equivalent required two separate pieces that had to stay in sync:
// C++11 / C++14 β header
class Widget {
public:
static int instance_count; // declaration only
static constexpr int kVersion = 3; // ok inside class for constexpr integral
};
// exactly one .cpp
int Widget::instance_count = 0; // definitioninline static constexpr is the preferred idiom for compile-time class constants in C++17 and later: it is simultaneously a constant expression, has no ODR footgun, and requires no out-of-class definition.
static constexpr vs inline static constexpr
The distinction matters the moment you ODR-use the constant. A static constexpr member declared in C++11/14 style is usable in constant expressions, but if the program takes its address or binds it to a reference the linker requires an out-of-class definition β one that carries no initializer.
// C++11
struct Limits {
static constexpr int N = 42; // declaration + in-class initializer
};
constexpr int Limits::N; // C++11: out-of-class definition required if odr-used
void odr_use() {
const int* p = &Limits::N; // odr-use: takes address β needs definition above
int arr[Limits::N]; // NOT odr-use: used in constant expression β fine
}In C++17 static constexpr inside a class body is implicitly inline, so the out-of-class definition is no longer needed. Spelling it inline static constexpr makes the intent explicit and works in both C++17 and later:
// C++17
struct Limits {
inline static constexpr int N = 42; // self-contained; safe to odr-use
};
void safe_odr_use() {
const int* p = &Limits::N; // fine β definition is already here
std::array<int, Limits::N> arr{}; // also fine as template argument
}The practical rule: in C++17+ always write inline static constexpr; in C++11/14 avoid odr-using static constexpr members or add the out-of-class definition.
Static Member Functions: No this, No Virtual
A static member function has no implicit this parameter, so it can only access static members and globals β but it is otherwise a first-class callable. It can be passed as a function pointer, used in std::call_once for lazy initialization, and serves as a natural factory entry point.
#include <mutex>
class Logger {
public:
// Factory pattern: construction logic belongs to the class but needs no instance
static Logger& get() {
static Logger instance; // C++11: magic static, thread-safe
return instance;
}
static void set_level(int level) {
level_ = level; // accesses only static member
}
void write(const char* msg) const; // non-static: needs this
private:
Logger() = default;
inline static int level_ = 0; // C++17
};Static member functions cannot be declared const, volatile, or virtual β those qualifiers all modify the implicit this, which does not exist. They also cannot be override or final. They can be called through an object or pointer (the object is silently ignored), but calling through the class name is clearer:
Logger::set_level(2); // preferred
Logger::get().set_level(2); // also fine β get() returns a reference, set_level is staticstd::call_once pairs naturally with a static member function when one-time initialization must be explicitly thread-guarded rather than relying on magic statics:
#include <mutex>
class Config {
public:
static void init() {
std::call_once(init_flag_, []{ loaded_ = true; }); // C++11
}
private:
inline static bool loaded_ = false; // C++17
inline static std::once_flag init_flag_{}; // C++17
};Thread Safety of Static Local Variables vs Static Members
C++11 introduced a key asymmetry: function-local statics are thread-safe on first use; class-level statics are not automatically so. The standard guarantees that if multiple threads race to initialize a static local variable, exactly one runs the initializer and the rest wait β this is the "magic static" or "Meyers singleton" guarantee.
// C++11 β thread-safe lazy initialization via magic static
class Registry {
public:
static Registry& instance() {
static Registry reg; // initialized once, safely, even under concurrency
return reg;
}
private:
Registry() = default;
};Class-level static data members obey a different rule: within a single translation unit, statics are initialized in declaration order before main; across translation units the order is unspecified. This is the Static Initialization Order Fiasco (SIOF):
// a.cpp
int A::seed = compute_seed(); // may run after b.cpp if linker orders TUs arbitrarily
// b.cpp
int B::value = A::seed * 2; // undefined behaviour if A::seed not yet initializedThe Construct On First Use idiom eliminates SIOF by replacing the static data member with a static local inside a static member function, trading the unspecified cross-TU order for the well-defined magic-static guarantee:
// C++11
class Config {
public:
static int& seed() {
static int s = compute_seed(); // initialized on first call, thread-safe
return s;
}
};
// Anywhere else:
int B::value = Config::seed() * 2; // safe: seed() initializes on demandFor plain mutable static data members shared across threads, use std::atomic<T> for lock-free access or a std::mutex for compound operations.
Static Members in Templates
Each distinct template instantiation gets its own independent copy of every static member. Foo<int>::counter and Foo<double>::counter are separate variables with separate storage, even though they come from the same template.
// C++17
template <typename T>
class InstanceCounter {
public:
InstanceCounter() { ++count_; }
~InstanceCounter() { --count_; }
static int live() { return count_; }
private:
inline static int count_ = 0; // one variable per instantiation
};
// Usage
InstanceCounter<int> a, b; // InstanceCounter<int>::count_ == 2
InstanceCounter<double> x; // InstanceCounter<double>::count_ == 1
// The two counts are completely independentA static constexpr member inside a template class can itself be used as a template argument, since it is a constant expression:
// C++11
template <typename T>
struct Traits {
static constexpr std::size_t alignment = alignof(T);
};
template <std::size_t N>
struct Aligned {};
using AlignedInt = Aligned<Traits<int>::alignment>; // ok: constant expressionThe out-of-class definition rule follows the same pattern as non-template classes, except the definition must be written as a template:
// C++11/14 out-of-class definition for a template static member
template <typename T>
int InstanceCounter<T>::count_ = 0; // one definition, instantiated per TWith inline static (C++17) this separate definition is unnecessary.
Mutable Static State Anti-Pattern
Global mutable state hidden inside a class is still global mutable state. Class-level static data members share every problem of global variables β unpredictable initialization order, difficult test isolation, and implicit coupling between otherwise unrelated code β with the added confusion that they look like instance state at the call site.
The Meyer's singleton is the acceptable exception: it is effectively immutable after first use and its initialization order is deterministic via Construct On First Use:
// C++11 β acceptable: Meyer's singleton, thread-safe, no SIOF
class EventBus {
public:
static EventBus& get() {
static EventBus bus;
return bus;
}
void publish(int event);
private:
EventBus() = default;
};Contrast with the anti-pattern of a globally mutable counter or registry that every component writes to:
// Anti-pattern: hidden global state, breaks test isolation
class RequestHandler {
public:
void handle() { ++total_requests_; } // side-effects invisible to callers
static int total_requests_;
};
int RequestHandler::total_requests_ = 0; // all tests share thisThe preferred alternative is dependency injection: pass the shared state explicitly so callers control lifetime and tests can substitute it:
// Better: explicit dependency, testable
class Stats {
public:
void record_request() { ++count_; }
int count() const { return count_; }
private:
int count_ = 0;
};
class RequestHandler {
public:
explicit RequestHandler(Stats& stats) : stats_(stats) {}
void handle() { stats_.record_request(); }
private:
Stats& stats_;
};Static state also prevents parallelism: two threads cannot each run their own isolated RequestHandler when both write to the same static counter. Dependency injection or thread-local storage are the clean remedies.
See Also
- Const Correctness β
const staticmembers and their initialization rules - Class Templates β per-specialization static member instances
- Abstract Classes β static members inherited and overriding pure-virtual patterns