constexpr & static
constexpr pushes work to compile time; static means something different depending on where you write it.
Intermediate
constexpr on a variable is a stronger promise than const: not
just "this won't change," but "this value is known at compile time." That unlocks contexts
that specifically require a compile-time constant — array sizes, template non-type
arguments, static_assert conditions.
const int n1 = getUserInput(); // fine — const, but not known until runtime
constexpr int n2 = 10; // fine — a genuine compile-time constant
constexpr int n3 = n2 * 2; // fine — depends only on other compile-time constants
int arr1[n1]; // error (in standard C++) — array bound needs a compile-time constant
int arr2[n2]; // fine — n2 qualifies
constexpr variable must be initialized immediately, and only from other
values the compiler can evaluate at compile time. Inside a class, only static
members can be constexpr — a non-static member is only instantiated when
an object is constructed, at runtime, which contradicts "known at compile time."
constexpr function can run at compile time when its arguments are
themselves compile-time constants — called with runtime values, it just runs
normally at runtime like any other function. Several restrictions follow directly from "must
be evaluable at compile time": before C++20 it couldn't be virtual (virtual dispatch is
inherently a runtime decision) — C++20 lifted this and allows constexpr virtual
functions, as long as the actual call resolved at compile time doesn't require runtime
polymorphism — it can (in the constant-evaluated path) only call other constexpr
functions, and it can't read or write anything outside itself (no global mutable state, no I/O).
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
constexpr int f5 = factorial(5); // computed at compile time — f5 is baked into the binary as 120
int x = getUserInput();
int fx = factorial(x); // same function, but now evaluated at runtime — x isn't known until then
constexpr function body could contain no local variable
declarations at all (only a single return, static_assert, and
type aliases). C++14 is what introduced local variables inside
constexpr functions in the first place, as long as they're initialized and not
static or thread-local — including locals of user-defined type, provided
that type's constructor is itself constexpr (which in turn requires that
constructor to initialize every member of the class). That same C++14 relaxation is also
what first allowed a constexpr function to return void (by making
void a literal type) — this was not a C++20 change.
class Point {
int x, y;
public:
constexpr Point(int x, int y) : x(x), y(y) {} // constructor initializes every member — required
constexpr int getX() const { return x; }
};
constexpr Point origin(0, 0); // constructed entirely at compile time
constexpr int ox = origin.getX(); // also evaluated at compile time
constexpr constructor is only valid if it initializes every data member
— the compiler needs to fully account for the object's state to evaluate it at compile
time, and a partially-initialized member would leave that state undefined.
class Shape {
public:
Shape() { total++; }
static int total; // declaration — one copy, shared by every Shape and every subclass
void report(std::string label) { std::cout << label << ".total = " << total << '\n'; }
};
int Shape::total = 0; // definition — required exactly once, outside the class
class Circle : public Shape {}; // doesn't declare its own total — shares Shape's
Shape a, b;
Circle c; // Circle's constructor implicitly calls Shape's, incrementing total too
a.report("a"); // a.total = 3
b.report("b"); // b.total = 3
c.report("c"); // c.total = 3 — same shared counter, not one per subclass
this and can only access other static
members; a static function can't be virtual, const, or
volatile, since all three of those qualify how a function relates to a specific
object instance, and a static function isn't tied to one.
static means something different from inside a
class: it restricts a function or variable's visibility to the current translation unit,
making it invisible to the linker elsewhere — internal linkage. Without static,
a free function or global variable has external linkage by default, visible to every other
file the linker combines into the program.
// helpers.cpp
static void internalHelper() { /* only callable from within helpers.cpp */ }
void publicApi() { /* callable from any file that declares it */ }
static
behaves the opposite way when the enclosing function is inline with external
linkage: the standard guarantees a single shared object across every translation unit that
defines that inline function, even though the function itself may be compiled into multiple
object files. That's precisely what makes the Meyers-singleton pattern safe to put in a header
included from many .cpp files — see Design
Patterns.
assert — if the condition
is false, the build fails immediately with your message, instead of the program misbehaving
(or an assert catching it only when that code path actually executes).
static_assert pairs naturally with constexpr and templates, since
both let you express conditions the compiler can actually check up front.
template <typename T>
struct FixedBuffer {
static_assert(std::is_trivially_copyable<T>::value, "FixedBuffer requires a trivially copyable type");
T data[64];
};