Static, Instance and Constant Members
Learning Objectives
Choose between instance and static (class-level) members.
Express constants and immutability in C++, Java, C#, Python and TypeScript.
Distinguish shallow from deep immutability.
Use nested classes and avoid static-initialization hazards.
Prerequisites: Modules 2–3.
Intuition and Definition
An instance member belongs to each object (every lamp has its own on flag). A static member (class member) belongs to the class and is shared by all objects (a single created counter). A constant never changes after initialization; an immutable object is one whose observable state never changes after construction.
Run-time constant / read-only Immutable data type const member, const object class with only const members/getters static final primitive/String record, unmodifiable collections readonly field, init setter record, readonly struct convention (UPPER_CASE) @property without setter @dataclass(frozen=True), tuples as const literals
readonly (compile time), Object.freeze (run time) Readonly<T>, frozen objects
Shallow vs deep immutability: a final/readonly/const reference only stops the *reference* from changing; the object it points to may still be mutable (readonly items: string[] can still push). Deep immutability requires every reachable object to be immutable.
Why It Matters
Immutable objects can be shared freely between threads and callers, never need defensive copies, and make reasoning local. Static members are convenient but are global state in disguise — a common source of test pollution and initialization-order bugs.
Syntax and Language Notes
class Config {
public:
static constexpr int MaxSensors = 16; // compile-time class constant
static int instances(); // static function: no 'this'
const std::string name; // set once in the constructor
explicit Config(std::string n) : name(std::move(n)) {}
};from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int # assigning p.x = 3 raises dataclasses.FrozenInstanceErrorclass Limits {
static readonly MAX_SPEED = 250;
readonly tags: readonly string[] = ["a"]; // readonly reference AND readonly array type
}public readonly record struct Point(int X, int Y); // deeply immutable value type
public static class Units { public const double KmPerMile = 1.609344; }Complete Example
Shallow vs deep immutability in Python and TypeScript:
#include <iostream>
#include <string>
#include <vector>
class Sensor {
public:
static constexpr double MinCelsius = -40.0; // class constant
static int count() { return count_; } // static function: no object needed
explicit Sensor(std::string id) : id_(std::move(id)) { ++count_; }
~Sensor() { --count_; }
const std::string& id() const { return id_; }
class Reading { // nested type
public:
explicit Reading(double c) : celsius(c < MinCelsius ? MinCelsius : c) {}
const double celsius; // const member: fixed at construction
};
private:
const std::string id_;
static inline int count_ = 0; // one counter for the whole class
};
struct Holder {
const std::vector<int>* data; // pointer-to-const: can't modify through it
std::vector<int>* const mutableData; // const pointer: can't re-point, CAN modify
};
int main() {
std::cout << "sensors: " << Sensor::count() << "\n";
{
Sensor a("coolant"), b("oil");
std::cout << "sensors: " << Sensor::count() << "\n";
Sensor::Reading r(-55.0);
…sensors: 0
sensors: 2
coolant reading clamped to -40
sensors after scope: 0
size seen through the const view: 4from dataclasses import dataclass, FrozenInstanceError
@dataclass(frozen=True)
class Route:
name: str
stops: list # the reference is frozen, the list is not
r = Route("A", ["x", "y"])
try:
r.name = "B"
except FrozenInstanceError as e:
print("FrozenInstanceError:", e)
r.stops.append("z") # shallow immutability!
print(r)
@dataclass(frozen=True)
class SafeRoute:
name: str
stops: tuple # deeply immutable
print(SafeRoute("A", ("x", "y")))FrozenInstanceError: cannot assign to field 'name'
Route(name='A', stops=['x', 'y', 'z'])
SafeRoute(name='A', stops=('x', 'y'))"use strict"; // strict mode: writing to a frozen property throws
const limits = Object.freeze({ maxSpeed: 250, zones: ["city", "highway"] });
try {
(limits as any).maxSpeed = 300;
} catch (e) {
console.log("assignment failed:", (e as Error).constructor.name);
}
console.log(limits.maxSpeed);
limits.zones.push("track"); // freeze is shallow
console.log(limits.zones);assignment failed: TypeError
250
[ 'city', 'highway', 'track' ]Step-by-Step Walkthrough
Sensor::count() is called on the class, before any object exists.
Each constructor increments and each destructor decrements the shared counter, so it tracks live sensors.
Sensor::Reading is a nested class; it can use MinCelsius directly because it is in the enclosing class's scope.
Holder shows that const applies to *what it qualifies*: const std::vector<int>* protects the vector from modification through that pointer, but another non-const path can still change it.
Python's frozen dataclass and JavaScript's Object.freeze are shallow: the list/array inside is still mutable. In strict mode (ES modules, classes, or a "use strict" file) assigning to a frozen property throws a TypeError; in sloppy-mode scripts the assignment is silently ignored — another reason to always run strict.
Common Mistakes
Mutable static state as hidden globals (static caches, static "current user"). It makes tests order-dependent and code non-reentrant.
Static initialization order fiasco: a static object in one translation unit using a static object in another that may not be constructed yet. Use function-local statics (static Config& instance() { static Config c; return c; }), which are initialized on first use.
Thinking final/readonly makes an object immutable — it only freezes the reference.
Java/Python class-variable surprises: a mutable class attribute (items = [] in a Python class body) is shared by all instances.
Trade-offs and Alternatives
Prefer passing dependencies (Module 22) over static access. Use static members for true class-level facts: constants, factory functions, counters for diagnostics.
Real-World Use
Unit-conversion constants, maximum limits in calibration code, std::numeric_limits, and Color.RED-style predefined instances. In embedded code, static constexpr lookup tables live in flash memory.
Practice
Q1. (Easy) Can a static member function access instance fields directly?
Answer: No — it has no this; it can only use static members or objects passed to it.
Q2. (Medium) Why does this Python class surprise people? class Cart: items = []
Answer: items is a class attribute shared by every Cart; appending through one instance appends for all. Initialize per-instance state in __init__ (self.items = []).
Q3. (Advanced) Make a deeply immutable Route in C++ whose stops cannot be changed through any public path.
#include <iostream>
#include <span>
#include <string>
#include <vector>
class Route {
const std::string name_;
const std::vector<std::string> stops_; // const member: never modified
public:
Route(std::string n, std::vector<std::string> s) : name_(std::move(n)), stops_(std::move(s)) {}
std::span<const std::string> stops() const { return stops_; } // read-only view
const std::string& name() const { return name_; }
};
int main() {
Route r("Airport", {"Center", "Park", "Terminal"});
for (const auto& s : r.stops()) std::cout << s << " ";
std::cout << "(" << r.stops().size() << " stops)\n";
}Center Park Terminal (3 stops)Interview Question
Q1. "Is a C++ function-local static thread-safe to initialize?"
Answer: Yes, since C++11 ("magic statics"): if several threads reach the declaration concurrently, exactly one initializes it and the others wait. That makes static T& instance() { static T t; return t; } a safe lazy singleton *construction*. It does not make the object's later use thread-safe, and destruction order at program exit can still bite (another static's destructor may use it after it is destroyed).
Quick Quiz
Q1. Where is a static data member's value stored?
Answer: Once per class (static storage), not inside each object.
Q2. readonly string[] vs readonly tags: string[] in TypeScript?
Answer: The first makes the array type read-only (no push); the second only prevents reassigning the property.
Q3. What is shallow immutability?
Answer: The top-level object can't change, but objects it references can.
Summary
Instance members per object; static members per class; constants never change.
Most language "constant" features are shallow; design deep immutability deliberately.
Avoid mutable statics; prefer function-local statics and explicit dependencies.
Related: Module 18 (value objects), Module 27 (Singleton), Module 60 (concurrency).