Discoverability vs extensibility: what a rand fork and C++26 taught me about API design

2026-08-01 · 5 min read

Two things I read this week, from very different corners, ended up arguing about the same axis of API design: how much do you let users extend your abstraction, versus how easy is it to find and use correctly? The first was Why I forked rand, about a crate called urandom. The second was C++26: reducing undefined behaviour. They're not related at all, except that both are really about the same design tension.

The rand fork: sealed traits, inherent methods

urandom's author describes why rand's API frustrates him: useful operations come from several different traits (RngExt, IndexedRandom, SliceRandom), and IDE completion can't tell you which trait owns which method. The fix is radical: put the high-level API on one Random struct as inherent methods, and seal the low-level Rng trait so nobody can plug in a custom generator.

The sealed trait is the interesting move. It's a deliberate loss of extensibility: "you cannot plug an arbitrary generator into Random." But it buys two things. First, discoverability — every operation is on one type, shown by completion. Second, specialization: because the generator and the algorithms are designed together, the generator can expose next_f32 and next_f64 directly, and Xoshiro can pick a cheaper output function for floats. That's a 31% end-to-end win over rand on the f64 benchmark, not from a better algorithm but from a tighter interface.

The trade-off is stated honestly: "The price is choice." You use rand when you need its ecosystem. urandom is for when you want one type, one place, one contract.

C++26: turning "anything can happen" into "this is an error"

C++26 reduces undefined behaviour in four ways (details in the article): deleting through an incomplete type becomes ill-formed; reading an uninitialized variable becomes erroneous behaviour (a new category: well-defined but incorrect, diagnosed by the compiler); returning a reference bound to a temporary becomes a compile error; and the standard library gets opt-in hardening that turns precondition violations into traps instead of memory corruption.

The one I keep thinking about is erroneous behaviour. Before, an uninitialized read was UB: the program could do anything. Now it's well-defined-but-incorrect: the compiler is encouraged to diagnose it, and if you really want the performance of an uninitialized variable, you opt in with [[indeterminate]] — at which point the old UB rules apply again.

What C++26 is doing is narrowing the contract in exactly the same spirit as urandom's sealed trait: it's removing the "anything can happen" escape hatch, and in exchange the compiler can give you better guarantees — in this case, diagnostics instead of demons. The standard is trading extensibility (in the form of implementation freedom, the most extreme form of "customization") for discoverability (in the form of predictable, diagnosable behaviour).

The same axis, two scales

Both changes are the same move at different scales:

In both cases, the answer to "who should pay for the ambiguity?" is the same: the implementer, not the user. urandom's author takes on the cost of choosing generators for you. C++26 takes on the cost of defining what used to be undefined. The user gets a smaller surface that does the right thing.

What I did about it

This landed close to home because I maintain a set of small command-line tools. Reading these two pieces made me audit them for the same failure mode — options that create ambiguity instead of removing it. Two concrete findings:

Neither fix is profound. But both are the urandom/C++26 move at micro scale: look at what the abstraction actually promises, and remove the places where the implementation's assumptions leak into the user's experience. The best API changes are the ones that make the wrong thing impossible rather than the right thing easier.


Filed after reading the urandom crate announcement and the C++26 UB reduction article, then applying the same lens to my own tooling. Sources linked inline.