Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 7 – Classes, objects and RAII

C++ static members, friends and nested types

Share one value across all objects with static members, give chosen functions private access with friends and hidden friends, and nest types in classes.

  • Intermediate
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free: 20 minutes

What you will learn

  • Use static data members and static member functions, and define them correctly before and after C++17
  • Use friend functions and friend classes to give chosen non-members access to private members
  • Explain why hidden friends are found only through their class, and what that prevents
  • Define nested types and member aliases that generic code can read
What you’ll learn

What you will learn

  • Use static data members and static member functions, and define them correctly before and after C++17
  • Use friend functions and friend classes to give chosen non-members access to private members
  • Explain why hidden friends are found only through their class, and what that prevents
  • Define nested types and member aliases that generic code can read
On this page

A static member belongs to a class rather than to any one object. A static data member exists once for the whole program, however many objects there are, so it suits a count of live objects or a setting that every object shares. A static member function runs without an object, has no this pointer and is called through the class, as in Ticket::live(). A friend declaration lets a non-member function or another class use the class’s private members; a friend function defined inside the class, a hidden friend, is found only through arguments of that class, which keeps it out of unrelated code. Nested types, such as Playlist::Song, and member aliases, such as using value_type = Song;, keep names that belong together inside the class that uses them.

This lesson counts tickets with a static member, shows the link error that C++17’s inline variables removed, grants private access with friends, compares hidden friends with operators at namespace scope, and nests a type inside a playlist. Every example runs with Apple clang 21 and GCC 16.

Static data members: one value for the whole class

Ticket counts the tickets that exist at the moment. Each constructor adds one to live_ and the destructor takes one away, and because live_ is static, all the tickets change the same counter:

ticket_counter.cpp C++ · ticket_counter.cpp
// A static data member: one counter shared by every Ticket, not one per object.
#include <print>
#include <string>
#include <utility>

class Ticket {
public:
    explicit Ticket(std::string passenger) : passenger_(std::move(passenger)) { ++live_; }
    Ticket(const Ticket& other) : passenger_(other.passenger_) { ++live_; }
    Ticket& operator=(const Ticket&) = default;
    ~Ticket() { --live_; }

    // A static member function has no object, so it is called on the class: Ticket::live().
    static int live() { return live_; }

    // A constant that belongs to the class; static constexpr members are inline since C++17.
    static constexpr int max_per_booking = 6;

private:
    std::string passenger_;
    inline static int live_ = 0;   // inline (C++17): declared and defined here, once for the program
};

int main() {
    std::println("at the start:      live() = {}", Ticket::live());
    Ticket a("Asha");
    {
        Ticket b("Dev");
        Ticket c = b;   // the copy constructor counts too
        std::println("inside the block: live() = {}", Ticket::live());
    }
    std::println("after the block:  live() = {}", Ticket::live());
    std::println("a.live() makes the same call: {}", a.live());
    std::println("at most {} tickets per booking", Ticket::max_per_booking);
    std::println("sizeof(Ticket) = {}, sizeof(std::string) = {}", sizeof(Ticket), sizeof(std::string));
}
Three tickets, then one Python · ticket_session.py
from session import run

# ticket_counter.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -Wall -Wextra ticket_counter.cpp -o ticket_counter && ./ticket_counter
g++-16 -std=c++23 -Wall -Wextra ticket_counter.cpp -o ticket_counter-gcc && ./ticket_counter-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra ticket_counter.cpp -o ticket_counter && ./ticket_counter
at the start:      live() = 0
inside the block: live() = 3
after the block:  live() = 1
a.live() makes the same call: 1
at most 6 tickets per booking
sizeof(Ticket) = 24, sizeof(std::string) = 24
$ g++-16 -std=c++23 -Wall -Wextra ticket_counter.cpp -o ticket_counter-gcc && ./ticket_counter-gcc
at the start:      live() = 0
inside the block: live() = 3
after the block:  live() = 1
a.live() makes the same call: 1
at most 6 tickets per booking
sizeof(Ticket) = 32, sizeof(std::string) = 32

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ticket_session.py

Three Ticket objects each hold their own passenger name, while one live_ counter in static storage is shared by all three.Three TicketsTicket::live_ = 3one int for the whole program,in static storage, not inside any objecta: passenger_ = "Asha"b: passenger_ = "Dev"c: passenger_ = "Meera"every constructor adds 1,every destructor takes 1 away

Ordinary members live in each object, a static member once

Text description of the diagram

The diagram shows the memory of the Ticket example while three tickets exist.

  1. Each of the three Ticket objects, a, b and c, holds its own data member passenger_: "Asha", "Dev" and "Meera". Copying or destroying one object does not touch the others.
  2. The static data member Ticket::live_ is not inside any of them. It is a single int in static storage, created before main starts and kept until the program ends, so sizeof(Ticket) does not include it.
  3. Every constructor of Ticket adds 1 to that one counter and every destructor subtracts 1, so while these three tickets exist it holds 3.

Inside the block three tickets exist, a, b and the copy c. When the block ends, b and c are destroyed and the count falls to 1. The last line shows where the counter lives: a Ticket is exactly as large as its one ordinary member, the std::string, 24 bytes with Apple’s library and 32 with GCC’s, because a static data member is stored once, outside every object, for the whole run of the program.

Two kinds of declaration define a static member inside the class:

  • inline static int live_ = 0; declares the member and defines it in one place, which C++17 made possible (paper P0386R2). Every file that includes the class shares the one variable.
  • static constexpr int max_per_booking = 6; is a constant known while compiling. Since C++17 a static constexpr member is automatically inline, so it needs no definition elsewhere either.

A static data member that changes is a global variable with a class name in front of it. The C++ Core Guidelines advise against changeable globals (rule I.2), because any code anywhere can change them and threads can race on them. A counter like this one is fine in a single-threaded program; the concurrency module makes such counters safe with std::atomic.

Before C++17: the definition outside the class

Without inline, a static data member declared in a class is only a declaration. Exactly one source file must also define it, with a line such as int Config::retries = 3; outside the class. Forget that line and every file still compiles, because the compiler sees the declaration, but the linker finds no variable:

missing_definition.cpp C++ · missing_definition.cpp
// Before C++17: a static data member declared in the class still needs one definition outside it.
struct Config {
    static int retries;          // a declaration only
};

// int Config::retries = 3;      // the definition this program forgot

int main() { return Config::retries; }
It compiles, and the link fails Python · definition_session.py
from session import run

# Compiling missing_definition.cpp works; linking it fails, with both compilers.
run("""
clang++ -std=c++23 -c missing_definition.cpp -o missing_definition.o && clang++ missing_definition.o -o missing_definition
g++-16 -std=c++23 -c missing_definition.cpp -o missing_definition-gcc.o && g++-16 missing_definition-gcc.o -o missing_definition-gcc
""")

Output

$ clang++ -std=c++23 -c missing_definition.cpp -o missing_definition.o && clang++ missing_definition.o -o missing_definition
Undefined symbols for architecture arm64:
  "Config::retries", referenced from:
      _main in missing_definition.o
ld: symbol(s) not found for architecture arm64
clang++: error: linker command failed with exit code 1 (use -v to see invocation)
[exit status 1]
$ g++-16 -std=c++23 -c missing_definition.cpp -o missing_definition-gcc.o && g++-16 missing_definition-gcc.o -o missing_definition-gcc
Undefined symbols for architecture arm64:
  "Config::retries", referenced from:
      _main in missing_definition-gcc.o
ld: symbol(s) not found for architecture arm64
collect2: error: ld returned 1 exit status
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 definition_session.py

The usual fix in older code is that one definition in the class’s .cpp file. Putting it in the header instead gives the opposite error as soon as two files include the header: a duplicate symbol. inline static avoids both problems, so prefer it whenever you can build as C++17 or later.

Static member functions

A static member function has no object, so it has no this and cannot name the object’s ordinary members. It can use the class’s static members, its private constructors included, which makes it the natural place for a named constructor such as Temperature::from_celsius(20) that says in which unit its argument is. Reading kelvin_, though, needs an object, and both compilers refuse:

static_function_error.cpp C++ · static_function_error.cpp
// A static member function has no this pointer, so it cannot read an object's members.
class Temperature {
public:
    static Temperature from_celsius(double c) { return Temperature(c + 273.15); }   // fine: makes an object
    static double celsius() { return kelvin_ - 273.15; }   // error: whose kelvin_?

private:
    explicit Temperature(double k) : kelvin_(k) {}
    double kelvin_;
};

int main() { return Temperature::celsius() > 0; }
Whose kelvin_? Python · static_function_session.py
from session import run

# What each compiler says about static_function_error.cpp.
run("""
clang++ -std=c++23 -fsyntax-only static_function_error.cpp
g++-16 -std=c++23 -fsyntax-only static_function_error.cpp
""")

Output

$ clang++ -std=c++23 -fsyntax-only static_function_error.cpp
static_function_error.cpp:5:38: error: invalid use of member 'kelvin_' in static member function
    5 |     static double celsius() { return kelvin_ - 273.15; }   // error: whose kelvin_?
      |                                      ^~~~~~~
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -fsyntax-only static_function_error.cpp
static_function_error.cpp: In static member function 'static double Temperature::celsius()':
static_function_error.cpp:5:38: error: invalid use of member 'Temperature::kelvin_' in static member function
    5 |     static double celsius() { return kelvin_ - 273.15; }   // error: whose kelvin_?
      |                                      ^~~~~~~
static_function_error.cpp:9:12: note: declared here
    9 |     double kelvin_;
      |            ^~~~~~~
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 static_function_session.py

A static member function cannot be virtual or const either, since both describe how it treats an object. You may call one through an object, as a.live() did in the ticket example, but the object is ignored, so Ticket::live() says what really happens.

Friends: private access for chosen non-members

A class can name the non-members that may use its private members. A friend function is an ordinary function with that access, and a friend class gives it to every member function of the other class:

friends.cpp C++ · friends.cpp
// A friend function and a friend class: non-members that the class lets read its private members.
#include <cstdint>
#include <print>

class Wallet {
public:
    explicit Wallet(std::int64_t paise) : paise_(paise) {}

    friend bool same_balance(const Wallet& a, const Wallet& b);   // a non-member with access
    friend class Auditor;                                          // every member of Auditor has access

private:
    std::int64_t paise_;
};

bool same_balance(const Wallet& a, const Wallet& b) { return a.paise_ == b.paise_; }

class Auditor {
public:
    static std::int64_t total(const Wallet& a, const Wallet& b) { return a.paise_ + b.paise_; }
};

int main() {
    Wallet mine(25000), yours(25000);
    std::println("same balance? {}", same_balance(mine, yours));
    std::println("total {} paise", Auditor::total(mine, yours));
}
A friend function and a friend class Python · friends_session.py
from session import run

# friends.cpp built and run with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra friends.cpp -o friends && ./friends
g++-16 -std=c++23 -Wall -Wextra friends.cpp -o friends-gcc && ./friends-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra friends.cpp -o friends && ./friends
same balance? true
total 50000 paise
$ g++-16 -std=c++23 -Wall -Wextra friends.cpp -o friends-gcc && ./friends-gcc
same balance? true
total 50000 paise

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 friends_session.py

Friendship follows four rules:

  • The class grants it, in its own definition. No outside code can make itself a friend.
  • It is not inherited: a class derived from Auditor gets no access to Wallet.
  • It is not transitive: a friend of Auditor is not a friend of Wallet.
  • It is not mutual: Wallet has no access to the private members of Auditor.

Use friends sparingly. Guideline C.4 keeps a function out of the class unless it works on the class’s private representation, and a friend gives that access to a function that cannot be a member, such as an operator whose left operand has another type. The previous lessons’ comparison and output operators are the common case; a class and a helper that are designed together, such as a container and its iterator, are the other. When a function can do its job through the public interface, it should be neither a member nor a friend.

Hidden friends

A friend function that is defined inside the class, and declared nowhere else, is called a hidden friend. It belongs to the enclosing namespace, yet ordinary lookup there does not find it: only argument-dependent lookup does, when an argument has the class’s type (paper P1601R0 describes the technique). Calls in the usual form work, and naming the function explicitly does not:

hidden_friend.cpp C++ · hidden_friend.cpp
// Hidden friends: defined inside the class, found only through the class of an argument.
#include <print>

namespace geo {

struct Meters {
    double value = 0;
    Meters(double v) : value(v) {}   // implicit on purpose: 2.5 converts to Meters

    friend bool operator==(Meters a, Meters b) { return a.value == b.value; }   // a hidden friend
};

}  // namespace geo

int main() {
    geo::Meters length(2.5);
    std::println("length == 2.5 is {}", length == 2.5);   // found through Meters, then 2.5 converts
    std::println("2.5 == length is {}", 2.5 == length);   // the same, with the operands the other way round
}
hidden_lookup_error.cpp C++ · hidden_lookup_error.cpp
// A hidden friend is not a member of its namespace for ordinary lookup.
namespace geo {
struct Meters {
    double value = 0;
    friend bool operator==(Meters a, Meters b) { return a.value == b.value; }
};
}  // namespace geo

int main() {
    geo::Meters a{1}, b{1};
    bool same = a == b;                       // fine: found through the class of a and b
    bool also = geo::operator==(a, b);        // error: qualified lookup cannot see it
    return same && also;
}
Found through Meters, invisible by name Python · hidden_session.py
from session import run

# hidden_friend.cpp with both compilers, then what they say about naming the hidden friend directly.
run("""
clang++ -std=c++23 -Wall -Wextra hidden_friend.cpp -o hidden_friend && ./hidden_friend
g++-16 -std=c++23 -Wall -Wextra hidden_friend.cpp -o hidden_friend-gcc && ./hidden_friend-gcc
clang++ -std=c++23 -fsyntax-only hidden_lookup_error.cpp
g++-16 -std=c++23 -fsyntax-only hidden_lookup_error.cpp
""")

Output

$ clang++ -std=c++23 -Wall -Wextra hidden_friend.cpp -o hidden_friend && ./hidden_friend
length == 2.5 is true
2.5 == length is true
$ g++-16 -std=c++23 -Wall -Wextra hidden_friend.cpp -o hidden_friend-gcc && ./hidden_friend-gcc
length == 2.5 is true
2.5 == length is true
$ clang++ -std=c++23 -fsyntax-only hidden_lookup_error.cpp
hidden_lookup_error.cpp:12:22: error: no member named 'operator==' in namespace 'geo'
   12 |     bool also = geo::operator==(a, b);        // error: qualified lookup cannot see it
      |                      ^~~~~~~~~~
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -fsyntax-only hidden_lookup_error.cpp
hidden_lookup_error.cpp: In function 'int main()':
hidden_lookup_error.cpp:12:22: error: 'operator==' is not a member of 'geo'
   12 |     bool also = geo::operator==(a, b);        // error: qualified lookup cannot see it
      |                      ^~~~~~~~~~
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 hidden_session.py

length == 2.5 and 2.5 == length both work: lookup finds the operator through length, a Meters, and then the double converts. geo::operator==(a, b) fails because qualified lookup searches the namespace itself, where the hidden friend was never made visible.

That invisibility is the point. An operator at namespace scope is a candidate for every comparison in its namespace, and implicit conversions can pull in types that were never meant to meet it:

namespace_scope.cpp C++ · namespace_scope.cpp
// An operator== at namespace scope, and a type that converts to Meters: two Feet compare as Meters.
#include <print>

struct Meters {
    double value = 0;
    Meters(double v) : value(v) {}
};
bool operator==(Meters a, Meters b) { return a.value == b.value; }   // namespace scope: ordinary lookup finds it

struct Feet {
    double value = 0;
    operator Meters() const { return Meters(value * 0.3048); }        // an implicit conversion to Meters
};

int main() {
    Feet f{10}, g{10};
    std::println("Feet == Feet compiles and gives {}", f == g);   // both Feet quietly became Meters
}
hidden_blocks.cpp C++ · hidden_blocks.cpp
// The same design with a hidden friend: no operand is a Grams, so the operator is never considered.
#include <print>

struct Grams {
    double value = 0;
    Grams(double v) : value(v) {}
    friend bool operator==(Grams a, Grams b) { return a.value == b.value; }   // hidden friend
};

struct Ounces {
    double value = 0;
    operator Grams() const { return Grams(value * 28.349523125); }           // an implicit conversion to Grams
};

int main() {
    Ounces x{1}, y{1};
    std::println("{}", x == y);   // error: lookup for operator== never looks inside Grams
}
Two Feet compare; two Ounces do not Python · conversions_session.py
from session import run

# namespace_scope.cpp builds and runs; hidden_blocks.cpp does not compile (first error of each compiler).
run("""
clang++ -std=c++23 -Wall -Wextra namespace_scope.cpp -o namespace_scope && ./namespace_scope
g++-16 -std=c++23 -Wall -Wextra namespace_scope.cpp -o namespace_scope-gcc && ./namespace_scope-gcc
clang++ -std=c++23 -fsyntax-only hidden_blocks.cpp 2>&1 | grep -m1 error:
g++-16 -std=c++23 -fsyntax-only hidden_blocks.cpp 2>&1 | grep -m1 error:
""")

Output

$ clang++ -std=c++23 -Wall -Wextra namespace_scope.cpp -o namespace_scope && ./namespace_scope
Feet == Feet compiles and gives true
$ g++-16 -std=c++23 -Wall -Wextra namespace_scope.cpp -o namespace_scope-gcc && ./namespace_scope-gcc
Feet == Feet compiles and gives true
$ clang++ -std=c++23 -fsyntax-only hidden_blocks.cpp 2>&1 | grep -m1 error:
hidden_blocks.cpp:17:26: error: invalid operands to binary expression ('Ounces' and 'Ounces')
$ g++-16 -std=c++23 -fsyntax-only hidden_blocks.cpp 2>&1 | grep -m1 error:
hidden_blocks.cpp:17:26: error: no match for 'operator==' (operand types are 'Ounces' and 'Ounces')

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 conversions_session.py

Two Feet compare equal through the namespace-scope operator== for Meters, after both are converted. Nobody wrote a comparison for Feet, and a version that compared feet differently would never be called. The hidden friend of Grams is considered only when an operand is a Grams, so two Ounces do not compare at all, which is what their author intended. Because a hidden friend is a candidate only for its own type, a comparison that fails to compile also comes with a shorter list of candidates in the error message. The standard library specifies many of its own operators as hidden friends.

Nested types and member aliases

A type declared inside a class is a member like any other, named with the class in front, Playlist::Song, and it follows the class’s access rules. Member aliases give generic code a way to ask a class about its types, the convention that every standard container follows with value_type, size_type and their relatives:

nested_types.cpp C++ · nested_types.cpp
// A nested type and member aliases: Playlist names its element type the way the standard containers do.
#include <compare>
#include <cstddef>
#include <print>
#include <string>
#include <vector>

class Playlist {
public:
    struct Song {                     // a nested type, called Playlist::Song outside the class
        int seconds = 0;
        std::string title;
        auto operator<=>(const Song&) const = default;   // seconds first, so a longer song is larger
    };

    using value_type = Song;          // member aliases, as std::vector<Song> has
    using size_type = std::size_t;

    void add(Song s) { songs_.push_back(s); }
    size_type size() const { return songs_.size(); }
    auto begin() const { return songs_.begin(); }
    auto end() const { return songs_.end(); }

private:
    std::vector<Song> songs_;
};

// Generic code reads the element type through the alias, so it works with any such container.
template <class Container>
typename Container::value_type largest(const Container& c) {
    typename Container::value_type best = *c.begin();
    for (const auto& item : c)
        if (best < item) best = item;
    return best;
}

int main() {
    Playlist evening;
    evening.add({245, "Monsoon Rain"});
    evening.add({312, "Long Drive"});
    evening.add({198, "Chai Break"});

    Playlist::Song longest = largest(evening);
    std::println("{} songs, the longest is {} ({} s)", evening.size(), longest.title, longest.seconds);

    std::vector<int> marks{71, 94, 88};
    std::println("the largest mark is {}", largest(marks));   // the same template, for std::vector<int>
}
One template, two containers Python · nested_session.py
from session import run

# nested_types.cpp built and run with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra nested_types.cpp -o nested_types && ./nested_types
g++-16 -std=c++23 -Wall -Wextra nested_types.cpp -o nested_types-gcc && ./nested_types-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra nested_types.cpp -o nested_types && ./nested_types
3 songs, the longest is Long Drive (312 s)
the largest mark is 94
$ g++-16 -std=c++23 -Wall -Wextra nested_types.cpp -o nested_types-gcc && ./nested_types-gcc
3 songs, the longest is Long Drive (312 s)
the largest mark is 94

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 nested_session.py

largest never names Song. It asks the container for typename Container::value_type, so the same template finds the longest song in a Playlist and the highest mark in a std::vector<int>. The typename keyword is required there, because the name depends on a template parameter, which the templates module explains. Song defaults <=> with seconds declared first, so a longer song compares greater.

A nested class is a member of its enclosing class, so its member functions may use the enclosing class’s private members. The reverse does not hold: the enclosing class has no special access to the private members of the nested one. That is why the exercise below makes IdGenerator a friend of its nested Id.

Your turn: an id generator

IdGenerator hands out ids 1, 2, 3 and so on, and counts how many it has handed out. Its nested class Id wraps the number, has a private constructor, so that only IdGenerator can make one, and compares with defaulted hidden friends. Complete id_generator.h:

  1. Add one counter for the whole program, as a static data member.
  2. Make next() return a new Id with the next number, starting from 1.
  3. Make issued() return how many ids have been handed out.
The starter code and its sample tests

C header · id_generator.h

// Starter code for "Your turn": give IdGenerator its counter and complete next() and issued().
#pragma once
#include <compare>
#include <cstdint>

class IdGenerator {
public:
    // A class-local type: an id is not just any integer, and only IdGenerator can make one.
    class Id {
    public:
        std::uint32_t value() const { return value_; }

        friend bool operator==(Id, Id) = default;   // hidden friends, found only through an Id argument
        friend auto operator<=>(Id, Id) = default;

    private:
        explicit Id(std::uint32_t v) : value_(v) {}
        friend class IdGenerator;                    // so that next() may call the private constructor
        std::uint32_t value_;
    };

    // TODO: return a new Id: 1 for the first call in the program, then 2, 3 and so on.
    static Id next() { return Id(0); }

    // TODO: return how many ids next() has handed out so far.
    static std::uint32_t issued() { return 0; }

private:
    // TODO: one counter for the whole program, shared by every call.
};

C++ · id_generator_tests.cpp

// Sample tests for id_generator.h. Build: clang++ -std=c++23 -Wall -Wextra id_generator_tests.cpp -o id_generator_tests
#include <cstdint>
#include <print>
#include <type_traits>
#include "id_generator.h"

// Checked while compiling: an Id cannot be made from a plain number.
static_assert(!std::is_constructible_v<IdGenerator::Id, std::uint32_t>, "only IdGenerator makes ids");
static_assert(!std::is_convertible_v<std::uint32_t, IdGenerator::Id>, "a number never turns into an id by itself");
static_assert(sizeof(IdGenerator::Id) == sizeof(std::uint32_t), "an Id costs no more than its number");

int checks = 0;
int failures = 0;

void check(bool ok, const char* what) {
    ++checks;
    if (!ok) {
        ++failures;
        std::println("FAIL {}", what);
    }
}

int main() {
    check(IdGenerator::issued() == 0, "no ids before the first call");
    IdGenerator::Id first = IdGenerator::next();
    IdGenerator::Id second = IdGenerator::next();
    check(first.value() == 1, "the first id is 1");
    check(second.value() == 2, "the second id is 2");
    check(IdGenerator::issued() == 2, "issued() counts the ids handed out");

    IdGenerator::Id copy = first;
    check(copy == first && first != second, "ids compare equal only to themselves and their copies");
    check(first < second, "a later id is larger");

    bool increasing = true;
    IdGenerator::Id previous = second;
    for (int i = 0; i < 1000; ++i) {
        IdGenerator::Id id = IdGenerator::next();
        if (!(previous < id)) increasing = false;
        previous = id;
    }
    check(increasing, "1,000 more ids, each larger than the one before");
    check(IdGenerator::issued() == 1002 && previous.value() == 1002, "the count and the last id agree");

    std::println("{} of {} checks passed", checks - failures, checks);
    return failures == 0 ? 0 : 1;
}

The tests start with three static_asserts, which the compiler checks: an Id cannot be made or converted from a plain number, and it is no larger than the number it holds. They pass already, because the starter’s design ensures them. At run time the starter passes only the check that no ids exist yet:

The sample tests against the starter code Python · id_session.py
from session import run

# The sample tests run against the starter code, before any TODO is done.
run("""
clang++ -std=c++23 -Wall -Wextra id_generator_tests.cpp -o id_generator_tests && ./id_generator_tests
g++-16 -std=c++23 -Wall -Wextra id_generator_tests.cpp -o id_generator_tests-gcc && ./id_generator_tests-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra id_generator_tests.cpp -o id_generator_tests && ./id_generator_tests
FAIL the first id is 1
FAIL the second id is 2
FAIL issued() counts the ids handed out
FAIL ids compare equal only to themselves and their copies
FAIL a later id is larger
FAIL 1,000 more ids, each larger than the one before
FAIL the count and the last id agree
1 of 8 checks passed
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra id_generator_tests.cpp -o id_generator_tests-gcc && ./id_generator_tests-gcc
FAIL the first id is 1
FAIL the second id is 2
FAIL issued() counts the ids handed out
FAIL ids compare equal only to themselves and their copies
FAIL a later id is larger
FAIL 1,000 more ids, each larger than the one before
FAIL the count and the last id agree
1 of 8 checks passed
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 id_session.py

Edit id_generator.h until both compilers print 8 of 8 checks passed. This generator is safe only while one thread calls it; two threads calling next() at once would race on the counter, which the concurrency module fixes.

NanoID, ULID & Snowflake ID Generator Compare counting ids like these with random and time-ordered ids, generated in the browser.

Interview questions

Fresher · How does a static member function differ from an ordinary member function?

An ordinary member function is called on an object and receives that object as this, so it can read and change the object’s data members. A static member function belongs to the class but runs without any object: it has no this, so it can use only static members, its parameters and whatever objects it creates, and it is called as Class::function(). It still has the class’s access rights, so it may call private constructors, which makes it useful for named constructors such as Temperature::from_celsius() and for counters kept in static data members. A static member function cannot be virtual, const or volatile, because those qualifiers describe how a function treats the object it is called on.

Key takeaways

  • A static data member exists once for the whole program and is not part of any object, so sizeof does not count it. Define it with inline static (C++17), or as static constexpr for a constant.
  • Without inline, a static data member needs exactly one definition outside the class, or the link fails.
  • A static member function has no this, uses only static members and the objects it is given or creates, and is called as Class::f().
  • Friendship is granted by the class and is not inherited, transitive or mutual; use it for operators and helper classes that need the representation.
  • A hidden friend, defined inside the class, is found only by argument-dependent lookup, so implicit conversions cannot drag unrelated types to it.
  • Nested types and member aliases such as value_type keep related names inside the class and let templates ask a class for its types.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 class Sensor { double reading_; inline static int count_ = 0; }; What is sizeof(Sensor) on a typical 64-bit computer?

    Choose one answer.

    Show the answer to question 1

    Answer: 8, the size of reading_ alone

    A static data member is stored once for the whole program, outside every object, so it adds nothing to the size of an object. Only reading_, a double of 8 bytes, is inside each Sensor.

  2. Question 2 of 6 A header declares struct Config { static int retries; }; and no file defines Config::retries. A function reads Config::retries. When does the build fail?

    Choose one answer.

    Show the answer to question 2

    Answer: When linking, with an undefined symbol

    Inside the class, a static data member that is not inline is only a declaration, which is enough to compile any file that uses it. The linker then finds no definition. Writing inline static int retries = 3; in the class (C++17) declares and defines it at once.

  3. Question 3 of 6 Which line of this class does not compile?

    Read the code, then choose one answer.

    class Counter {
    public:
        static int total() { return total_; }        // line A
        static int mine() { return mine_; }          // line B
        int both() const { return total_ + mine_; }  // line C
    private:
        inline static int total_ = 0;
        int mine_ = 0;
    };
    Show the answer to question 3

    Answer: Line B

    A static member function has no this, so it can name static members such as total_ but not mine_, which belongs to an object. An ordinary member function such as both() can use both kinds.

  4. Question 4 of 6 class Wallet declares friend class Auditor;. Which of these are true?

    Choose every answer that is right.

    Show the answer to question 4

    Answer:

    • Every member function of Auditor may use Wallet's private members
    • A class derived from Auditor gets no access to Wallet's private members

    Friendship is granted by the class that declares it, and it is neither inherited, transitive nor mutual. Only Auditor's own member functions get access to Wallet.

  5. Question 5 of 6 operator== is a hidden friend of geo::Meters, which has an implicit constructor from double. Which line does not compile?

    Read the code, then choose one answer.

    geo::Meters a{1}, b{1};
    bool p = a == b;                  // line A
    bool q = a == 1.0;                // line B
    bool r = geo::operator==(a, b);   // line C
    Show the answer to question 5

    Answer: Line C

    Argument-dependent lookup finds the hidden friend for lines A and B, because an operand is a Meters, and then 1.0 converts. A qualified name searches the namespace itself, where the hidden friend is not visible.

  6. Question 6 of 6 class Outer { int secret_ = 7; public: class Inner { public: int peek(const Outer& o) { return o.secret_; } }; }; Does peek compile?

    Choose one answer.

    Show the answer to question 6

    Answer: Yes, because a nested class is a member and has the same access as other members

    A nested class is a member of its enclosing class, so its functions may use the enclosing class's private members. The reverse needs a friend declaration: Outer has no special access to Inner's private members.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.