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.
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:
// 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));
} 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
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.
- 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.
- 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.
- 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 astatic constexprmember is automaticallyinline, 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:
// 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; } 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:
// 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; } 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:
// 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));
} 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
Auditorgets no access toWallet. - It is not transitive: a friend of
Auditoris not a friend ofWallet. - It is not mutual:
Wallethas no access to the private members ofAuditor.
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 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
} // 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;
} 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:
// 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
} // 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
} 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:
// 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>
} 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:
- Add one counter for the whole program, as a static data member.
- Make
next()return a newIdwith the next number, starting from 1. - Make
issued()return how many ids have been handed out.
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:
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.
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
sizeofdoes not count it. Define it withinline static(C++17), or asstatic constexprfor 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 asClass::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_typekeep 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.
References
- Working Draft, Programming Languages: C++ (N4950) (ISO/IEC JTC1/SC22/WG21)
- P0386R2: Inline Variables (ISO/IEC JTC1/SC22/WG21)
- P1601R0: Recommendations for Specifying 'Hidden Friends' (ISO/IEC JTC1/SC22/WG21)
- Friend (C++) (Microsoft)
- C++ Core Guidelines: C.4, make a function a member only if it needs direct access to the representation (Standard C++ Foundation)
- C++ Core Guidelines: I.2, avoid non-const global variables (Standard C++ Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress