






















I do like type invariants, but sometimes a contract facility has better ergonomics. If I can't have both I guess I'll take type invariants, but I would prefer both.
Posted May 12, 2026 17:32 UTC (Tue) by NYKevin (subscriber, #129325) [Link]
One clever (but unfortunately Rust-only) pattern I have discovered is the static enum. It goes like this:
// TODO: Replace Infallible with ! when the latter is stabilized.
use std::convert::Infallible;
pub trait Variant: Sized {
// Type bounds added for ergonomics, not essential.
type OnlyLeft: Copy + 'static;
type OnlyRight: Copy + 'static;
fn into_left<T, U>(e: Either<Self, T, U>, x: Self::OnlyLeft) -> T;
fn into_right<T, U>(e: Either<Self, T, U>, x: Self::OnlyRight) -> U;
fn select<T, U>(t: T, u: U) -> Either<Self, T, U>;
}
pub enum Either<V: Variant, T, U>{
Left(T, V::OnlyLeft),
Right(U, V::OnlyRight),
}
pub struct LeftVariant;
pub struct RightVariant;
impl Variant for LeftVariant {
type OnlyLeft = ();
type OnlyRight = Infallible;
fn into_left<T, U>(e: Either<Self, T, U>, _: Self::OnlyLeft) -> T{
let Either::<Self, T, U>::Left(v, _) = e;
v
}
// TODO: When replacing Infallible with !, we can simplify this to just return x directly.
fn into_right<T, U>(_: Either<Self, T, U>, x: Self::OnlyRight) -> U{
match x {}
}
fn select<T, U>(t: T, _: U) -> Either<Self, T, U>{
Either::Left(t, ())
}
}
impl Variant for RightVariant {
type OnlyLeft = Infallible;
type OnlyRight = ();
fn into_left<T, U>(_: Either<Self, T, U>, x: Self::OnlyLeft) -> T{
match x {}
}
fn into_right<T, U>(e: Either<Self, T, U>, _: Self::OnlyRight) -> U{
let Either::<Self, T, U>::Right(v, _) = e;
v
}
fn select<T, U>(_: T, u: U) -> Either<Self, T, U>{
Either::Right(u, ())
}
}
// Need a macro so that we only evaluate one or the other of t and u. They can both e.g. move
// from the same places, have overlapping mutable borrows, etc.
// XXX: It is somewhat ugly to take a type as an argument. Better syntax would look like this:
// new_either!<V>(t, u).
// But macro_rules! cannot consume that syntax, and proc macros are overkill. Oh well.
macro_rules! new_either{
($v:ty, $t:expr, $u:expr) => {
match <$v as $crate::Variant>::select((), ()) {
$crate::Either::Left(_, x) => $crate::Either::Left($t, x),
$crate::Either::Right(_, x) => $crate::Either::Right($u, x),
}
}
}
// Either should provide various methods for manipulating its contents, not shown here.
// It should also blanket impl some std traits when T and U impl those traits.
The point is that destructuring an Either instance inside of a match expression will only emit code for the branch that is actually taken. This means that we can write simple and obvious code that looks like a runtime branch, but is actually resolved at compile time. For example, new_either!() is a zero-overhead (branchless) operation despite containing a match with two arms. After monomorphization, one of those arms will always materialize an empty type (as the value of x), and the compiler will regard that as conclusive evidence that the arm is unreachable (or UB).
It also means we can pass around these OnlyLeft and OnlyRight tokens freely to prove which variant is active, and later use the into_left() and into_right() functions as needed. Since the tokens are either ZSTs or empty types, they never emit any code whatsoever.
I suspect, but cannot easily prove, that most or all design-by-contract use cases can be reformulated in terms of ZSTs, empty types, and similar constructs to what I show above. But retrofitting this much type system complexity into C++ is probably not a good idea.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。