Static members

A static member belongs to the class, not to an object: in C++ there is one copy of a static data member for the whole program, and a static member function can be called without an object at all.

That makes static members the natural home for a counter of live objects, a shared configuration value, or a factory function. The lesson covers the declaration and the definition that C++ requires, the inline form that removes the definition, and the linker error that a forgotten definition produces.

One counter for all objects

The declaration static int count; inside the class only announces the member. The definition int counter::count = 0; outside the class creates it and gives it a starting value, and both are required before C++17.

The constructor increments the counter and the destructor decrements it, so count is the number of live objects. The program creates two objects in a block and prints the count inside and after it.

The member is shared, so it cannot be reached through one object in a way that distinguishes objects: c.count and counter::count are the same int, and the public static function is the clean way to read it.

The counter is also a first look at object lifetime accounting: the constructor and the destructor are the only places that can see every object, so they are the only places where a live count can be kept correct.

#include <iostream> class counter { public: counter() { ++count; } ~counter() { --count; } static int live() { return count; } private: static int count; }; int counter::count = 0; int main() { std::cout << counter::live() << '\n'; { counter a; counter b; std::cout << counter::live() << '\n'; } std::cout << counter::live() << '\n'; }

g++ -std=c++23 -Wall -Wextra data.cpp -o data
./data

0
2
0

inline static

C++17 lets the member be defined inside the class with the inline keyword. No out-of-class definition is needed, and the value can be initialised where it is declared, which keeps the class in one place.

The program is the same counter with inline static int count = 0;. The behaviour is identical and the definition line is gone; the member still has one instance for the whole program.

Use the inline form in new code and keep the out-of-class definition in code that has to compile as C++14. Mixing the two, defining the member outside and marking it inline inside, is a redefinition error.

#include <iostream> class counter { public: counter() { ++count; } ~counter() { --count; } static int live() { return count; } inline static int count = 0; }; int main() { counter a; counter b; std::cout << counter::live() << '\n'; }

g++ -std=c++23 -Wall -Wextra inline.cpp -o inline_static
./inline_static

2

A static member function

A static member function has no this pointer, so it cannot touch non-static members. It can be called as counter::live() without an object, which is what the earlier programs did.

The program below adds a static function that parses text into a value, the shape a factory takes before the factory lesson. It reads the digits itself so the example stays short; lesson 017 has std::stoi for real code.

A static function is also the way to reach private static data from outside without exposing the data itself. The public interface is the function, and the member stays private.

#include <iostream> class version { public: static version parse(const char* text) { int value = 0; for (int i = 0; text[i] != '\0'; ++i) { if (text[i] >= '0' && text[i] <= '9') { value = value * 10 + (text[i] - '0'); } } return version{value}; } int major() const { return major_; } private: explicit version(int major) : major_(major) {} int major_; }; int main() { version v = version::parse("15"); std::cout << v.major() << '\n'; }

g++ -std=c++23 -Wall -Wextra function.cpp -o function
./function

15

A static constexpr value

A static constexpr member is a compile-time constant that belongs to the class. It needs no definition outside the class in any C++17 build, and it can be used where a constant expression is required.

The program prints the size of a buffer and uses the same value as an array bound. The member is part of the class interface, so every caller shares one definition of the maximum.

Prefer a constexpr member to a macro or a global constant: it has a type, it is scoped to the class, and the compiler checks it.

#include <iostream> class buffer { public: static constexpr int max_size = 8; int capacity() const { return max_size; } }; int main() { int storage[buffer::max_size]{}; buffer b; std::cout << b.capacity() << ' ' << sizeof(storage) / sizeof(storage[0]) << '\n'; }

g++ -std=c++23 -Wall -Wextra constexpr_member.cpp -o constexpr_member
./constexpr_member

8 8

The definition that is missing

A static data member declared but never defined has no storage. The compiler accepts the declaration and the uses, and the linker is the first tool that notices the missing definition.

The message names the symbol and the file, and the fix is one line outside the class: int counter::count = 0;. The error looks like a link failure rather than a C++ mistake, which is why it puzzles newcomers.

The /tmp/cc...o file in the message is the temporary object file of this build and has a different name every time; the symbol it cannot resolve is the same.

The inline form avoids the definition entirely, so a class written for C++17 does not have this failure mode at all.

#include <iostream> class counter { public: counter() { ++count; } static int live() { return count; } private: static int count; }; int main() { counter a; std::cout << counter::live() << '\n'; }

g++ -std=c++23 -Wall -Wextra missing_definition.cpp -o missing_definition

/usr/lib/gcc/x86_64-alpine-linux-musl/15.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: /tmp/ccPFCaFP.o: warning: relocation against `_ZN7counter5countE' in read-only section `.text._ZN7counter4liveEv[_ZN7counter4liveEv]'
/usr/lib/gcc/x86_64-alpine-linux-musl/15.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: /tmp/ccPFCaFP.o: in function `counter::counter()':
missing_definition.cpp:(.text._ZN7counterC2Ev[_ZN7counterC5Ev]+0xa): undefined reference to `counter::count'
/usr/lib/gcc/x86_64-alpine-linux-musl/15.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: missing_definition.cpp:(.text._ZN7counterC2Ev[_ZN7counterC5Ev]+0x13): undefined reference to `counter::count'
/usr/lib/gcc/x86_64-alpine-linux-musl/15.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: /tmp/ccPFCaFP.o: in function `counter::live()':
missing_definition.cpp:(.text._ZN7counter4liveEv[_ZN7counter4liveEv]+0x6): undefined reference to `counter::count'
/usr/lib/gcc/x86_64-alpine-linux-musl/15.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: warning: creating DT_TEXTREL in a PIE
collect2: error: ld returned 1 exit status

Exercises

  1. Add a static function that returns the number of objects ever created, not only the live ones. Which member do you need?
  2. Remove inline from the static member in inline.cpp and add the out-of-class definition. Does the program still build?
  3. Give version a static constexpr member for the current major version and use it as the default in parse().

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does a static data member belong to?

    Answer

    The class, with one copy for the whole program

    The member is shared, which is why it can count the live objects.

  2. What is missing from a static member function?

    Answer

    The this pointer

    The function is not called on an object, so there is no object to refer to.

  3. How is a static member defined since C++17?

    Answer

    With inline static inside the class

    The inline keyword allows the definition in the class without a duplicate symbol.

  4. Why use a static constexpr member for a constant?

    Answer

    It is usable as a compile-time constant and belongs to the class

    The value is part of the class interface and known at compile time.

  5. What does this program print? #include <iostream> class counter { public: counter() { ++live_; } ~counter() { --live_; } static int live() { return live_; } private: inline static int live_ = 0; }; int main() { std::cout << counter::live() << '\n'; { counter a; counter b; std::cout << counter::live() << '\n'; } std::cout << counter::live() << '\n'; }

    Answer

    0 / 2 / 0

    The counter starts at zero, two objects are alive, and both are destroyed before the last print.

  6. What does this program print? #include <iostream> class counted { public: counted() { ++live_; } static int live() { return live_; } private: inline static int live_ = 0; }; int main() { counted a; counted b; std::cout << counted::live(); }

    Answer

    2

    Two objects are created, and the shared counter reports two.

  7. What does this program print? #include <iostream> class math { public: static int triple(int value) { return value * 3; } }; int main() { std::cout << math::triple(5); }

    Answer

    15

    The static function is called without an object, and it returns 15.

  8. What does this program print? #include <iostream> class limits { public: static constexpr int value = 8; }; int main() { limits l; std::cout << limits::value << ' ' << l.value; }

    Answer

    8 8

    The constant is read through the class and through an object, and both prints show the same value.

  9. What does the linker say when a static member is declared but not defined?

    Answer

    undefined reference to `counter::count'

    The declaration reserves the name, and the linker needs the storage the definition provides.

  10. Why can a static member function not be const?

    Answer

    It has no object whose state it could promise not to change

    The const qualifier applies to the implicit object, and a static function has none.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev