JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

constexpr, Allocators, span and string_view

Three modern tools that push work off the run-time clock and onto the type system: constexpr moves computation into the compiler, custom allocators put you back in charge of the heap, and span/string_view let you pass a window onto data without copying or owning it.

Why this guide closes the rung

The earlier guides in this rung gave you the C++ value system: RAII for deterministic cleanup, move semantics for handing resources around cheaply, smart pointers for ownership, and templates for abstraction that compiles down to nothing. This last guide collects three tools that all serve the same master — the zero-overhead principle, the promise that an abstraction should cost no more at run time than the hand-written code it replaces. Each tool moves a different kind of work off the run-time clock: constexpr moves computation into the compiler, custom allocators move the heap decision back under your control, and span/string_view move data-passing away from copying.

Notice the through-line: in C, the toolbox for these jobs was the preprocessor macro (compile-time work), malloc()/free() (the one and only heap), and a raw `char *` plus a length passed as two separate arguments (a borrowed window). C++ gives each of those a typed, checked replacement that the compiler understands and can optimise. That is the whole arc of this rung in one sentence: take the things C did with raw pointers and untyped tricks, and let the type system carry the meaning — without adding run-time cost.

constexpr: running your code inside the compiler

A constexpr function is an ordinary function that the compiler is also allowed to evaluate at compile time, whenever its inputs are known then. Mark a function `constexpr` and the body must obey a few rules (no I/O, no calls to non-constexpr functions, and so on), but in return: if you call it with compile-time-known arguments in a context that needs a constant, the compiler runs the function during compilation and bakes the answer into the program. The same function still works normally at run time when the inputs are only known then. This is constexpr, and it is the type-checked, debuggable successor to the macro hacks C used for compile-time arithmetic.

Make it concrete. Write `constexpr unsigned table_mask(unsigned n)` whose body loops `while (m < n) m <<= 1` and returns `m - 1` — a real function with a loop and a shift. Now write `constexpr unsigned MASK = table_mask(100);`. Because `MASK` is itself a constant initialised from constant input, the compiler runs the loop during compilation, finds 127, and the emitted program simply contains the literal `0x7F` — no loop, no shift, no run-time math. You can even guard it with `static_assert(MASK == 0x7F)` and size an array `char buf[MASK + 1]` with it. The exact same `table_mask` called on a value read from a file would just run normally at run time.

Be precise about what `constexpr` does and does not promise. It says the function may be evaluated at compile time, not that it must be — call it with a run-time variable and it simply runs at run time like any function. The stricter cousin is consteval: a consteval function is an immediate function that must always be evaluated at compile time, so calling it with a run-time value is a hard error. Use plain constexpr for code that should work in both worlds; reach for consteval only when an answer truly has no meaning at run time. And remember the honest limit: the compiler is interpreting your code on a restricted abstract machine, so a heavy constexpr computation makes compiles slower — you are trading build time for run time, which is usually a good trade but never a free one.

Custom allocators: taking back the heap

In C there is exactly one heap and one way in: malloc() and free(), a general-purpose allocator that must serve every size and lifetime pattern. For most code that is fine. But on a hot path — say you allocate and free ten thousand short-lived nodes per frame — malloc()'s generality becomes overhead: it searches a free list, updates fragmentation bookkeeping, and may take a lock. A custom allocator lets you replace that one strategy with one tuned for your access pattern, and the C++ containers are built to accept it. The simplest and most useful one is an arena (a bump allocator): grab one big block from malloc() up front, keep a single pointer to the next free byte, and "allocate" by returning that pointer and bumping it forward by the requested size. There is no per-object free at all — you reclaim everything at once by resetting the pointer to the start. Allocation is a pointer add; freeing the whole arena is a single store. The trade is honest: you give up freeing one object early in exchange for allocation that is nearly free.

struct Arena {                    // a bump / arena allocator
    char  *base;                  // start of the big block from malloc
    size_t cap;                   // total bytes, e.g. 0x100000 (1 MiB)
    size_t used = 0;              // bytes handed out so far

    void *alloc(size_t n, size_t align) {
        size_t p = (used + (align - 1)) & ~(align - 1);  // round up to alignment
        if (p + n > cap) return nullptr;                 // honest: arena can fill up
        used = p + n;
        return base + p;          // bump the cursor, hand back the slot
    }
    void reset() { used = 0; }    // free EVERYTHING at once, no per-object free
};
Allocation is an alignment round-up and a pointer bump; reset() reclaims the whole arena.

Wiring a hand-rolled allocator into every container by hand used to be painful, so C++17 added std::pmr (polymorphic memory resources). A `std::pmr::memory_resource` is an interface with virtual `do_allocate`/`do_deallocate`; containers like `std::pmr::vector` hold a pointer to one and route every allocation through it. You can wrap your arena as a memory resource and hand it to a whole subtree of containers at once. There is a cost to be honest about: the allocation call is now a virtual call (an indirect jump), not inlined — but it is one indirection guarding a giant win when the underlying resource is an arena. The allocator interface is how the standard library hands you the heap.

span and string_view: a window, not a copy

Think about how C passes an array to a function: it cannot pass the array itself, so it passes a pointer to the first element and a separate length, `f(int *data, size_t n)`. The two travel as unrelated arguments, and nothing stops a caller from passing the wrong `n` and reading out of bounds. A `std::span<T>` is simply those two values — a pointer and a length — bundled into one small object with a safe interface. It is a non-owning view: it owns nothing, allocates nothing, and frees nothing; it just looks at a contiguous run of T that someone else owns.

Because a span is just a pointer plus a size — typically 16 bytes, passed in two registers — it is the zero-overhead principle made concrete: as cheap as the C two-argument idiom, but type-safe, with `.size()` carried along so range-checked loops and bounds asserts come for free. The same idea applied to text is std::string_view: a pointer and a length viewing a run of characters, whatever owns them. A string_view can name a slice of a `std::string`, a string literal, or a chunk of a memory-mapped file without copying a single byte and without caring who the owner is. This is the C++ answer to Rust's slices and &str — a borrowed window over a sequence.

These views are wonderful and genuinely dangerous, and a gentle guide must say so. A view does not extend the lifetime of what it points at — it is a borrow, exactly like a raw pointer, and the borrow checker that would catch a mistake lives in your head, not the compiler. The classic trap: a function returns a `string_view` into a temporary `std::string` that dies at the end of the full expression, leaving the view dangling and any read undefined behavior. The rule is the one from guide three: owners own, views borrow, and a view must never outlive its owner. Used with that discipline, span and string_view erase whole categories of copies; used carelessly, they reintroduce exactly the dangling-pointer bugs C is infamous for.

Putting it together: the shape of systems C++

Step back and look at the three tools side by side, because they rhyme. constexpr moves computation off the run-time clock and into the compiler. A custom allocator moves the heap decision off the general-purpose path and into a strategy you chose. span and string_view move data-passing off copying and onto a borrowed window. In every case the win is the same: the program does less work at run time, and the saving is expressed in types the compiler can check, not in comments you hope someone reads.

  1. See a computed constant, a magic number, or a #define doing arithmetic? Make it a constexpr function with a static_assert — the cost moves to the compiler, the answer becomes a literal.
  2. Profiler points at malloc on a hot path with a clear lifetime pattern? Wrap an arena (or pool) as a pmr memory resource and hand it to those containers — but run destructors and measure before and after.
  3. Passing an array-and-length pair, or a substring? Pass a span<T> or a string_view instead of a pointer plus size or a fresh copy — but keep the owner alive for as long as the view exists.

That is the whole rung in your hands now. C++ earns the name systems language not by being lower-level than C — it is not — but by letting you write what you mean (this owns, this borrows, this is constant, this is the heap I want) in a type system that compiles those intentions down to the same machine code you would have written by hand. RAII, value and move semantics, smart pointers, templates and concepts, and these three closing tools are all one idea wearing different clothes: pay for abstraction at compile time so you do not pay for it at run time. Carry that sentence with you and the rest of the language reads like variations on a theme.