Smart Pointers

← libxpp

Rust-inspired smart pointers with sizeof == sizeof(T*) guarantees. All are header-only, C++11-compatible.

Overview

TypeOwnershipThread-safeHeader
Own<T, Allocator>Unique, nullableNoown.h
Box<T, Allocator>Unique, non-nullNobox.h
Rc<T, Allocator>SharedNorc.h
Weak<T, Allocator>Weak observer for RcNoweak.h
Arc<T, Allocator>SharedYes (atomic)arc.h
ArcWeak<T, Allocator>Weak observer for ArcYes (atomic)arc.h
NonNull<T>Non-owning, non-nullNononnull.h

The XPP_MT / shared.h abstraction was removed: the library's internals (Bytes, channels, promise state) always use Arc<T> (atomic refcount). Rc<T> remains for user code that wants explicit zero-atomic-overhead single-threaded sharing.

All owning types default to GlobalAllocator and accept a custom Allocator template parameter. Empty allocators (like GlobalAllocator) incur zero storage overhead via EBO.

Key Design Choices

  • Single pointer storage: sizeof == sizeof(T*) for all types. No two-word shared_ptr layout.
  • Niche-optimized Option: Option<Arc<T>> and Option<Rc<T>> are also sizeof(T*) — nullptr = None.
  • Non-intrusive: RcInner<T, Allocator> = { strong, weak, value, alloc } in a single heap allocation. T doesn't inherit anything.
  • Rust-style refcount: weak count includes +1 for "all strongs as one weak". weak_count() subtracts this to match Rust semantics.
  • Allocator protocol: Allocator parameter (default GlobalAllocator) controls allocation/deallocation. Stored in control block (Arc/Rc) or via CompressedPair (Own/Box) with EBO. See Allocator.
  • Arc memory orders: relaxed for clone, release for drop, acquire fence only when count hits 0. Matches Rust libstd / triomphe / boost.

Covariant Up-cast

Rc<Derived, Allocator> → Rc<Base, Allocator> and Arc<Derived, Allocator> → Arc<Base, Allocator> work via covariant constructors (copy and move). Same Allocator required.

What xpp Has That STL Doesn't

Niche-Optimized Option<T>

The single biggest practical win. In xpp, Option<Own<T>>, Option<Box<T>>, Option<Rc<T>>, Option<Arc<T>>, and Option<NonNull<T>> all have sizeof == sizeof(T*). The None state is encoded via the null pointer — a value that normal construction never produces.

// STL: 16 bytes (8-byte pointer + bool + alignment padding)
std::optional<std::unique_ptr<int>> parent;

// xpp: 8 bytes — same size as a raw pointer
Option<Own<Node>> parent;

In tree, graph, or AST data structures where every node has an Option<parent> / Option<child>, this saves 8+ bytes per field. A million-node tree saves ~8 MB just on the parent edge alone.

Why STL can't do this: std::optional must be generic over all types, and std::unique_ptr(nullptr) is a valid (non-empty) state. xpp's smart pointers have a constructor-level invariant that null is unreachable — the type system guarantees a stored null pointer means None.

Non-Null by Default — Box<T>

STL has no equivalent. std::unique_ptr default-constructs to null, forcing null checks at every use site. Box<T> has no default constructor — if you have one, it owns a valid object. The compiler enforces this.

// STL: always defensive
void process(std::unique_ptr<Widget> w) {
  if (!w) return;          // ← who knows what the caller passed
  w->do_thing();
}

// xpp: type system gives the guarantee
void process(Box<Widget> w) {
  w->do_thing();           // ← never null, compiler-checked
}

Combined with Option<Box<T>>, you get explicit opt-in nullability at zero space cost — exactly Rust's model.

Single-Threaded Rc<T> (No Atomic Overhead)

std::shared_ptr's control block is always atomic, even when you know you're single-threaded. Every copy and destroy pays the memory barrier. xpp splits this into two types:

  • Rc<T> — plain int refcount, zero atomic overhead, for event-loop or single-thread code
  • Arc<T> — atomic refcount, for cross-thread sharing

In the dominant xpp use case (single-thread event loops), Rc<T> avoids all shared_ptr's atomic penalties.

Semantic Layering — Pick the Right Tool

NeedSTL gives youxpp gives you
Maybe-null, unique ownershipunique_ptr<T>Own<T>
Never-null, unique ownership—Box<T>
Maybe-null, shared ownershipshared_ptr<T>Rc<T> or Arc<T>
Maybe-null, non-owning observerweak_ptr<T>Weak<T> or ArcWeak<T>
Never-null, non-owning pointerraw T*NonNull<T>

Box<T> and NonNull<T> have no STL counterpart — they encode non-null guarantees in the type system that raw pointers and unique_ptr leave to convention.

Promise Ecosystem Integration

xpp smart pointers compose directly with Promise<T> chains — no glue code:

Promise<Own<Data>> fetch() {
  return Promise<void>::after(100).then([]() {
    return Own<Data>(new Data{42});  // Own flows through then()
  });
}

// Own → Box: take ownership, guarantee non-null downstream
auto boxed = fetch().await()
  .into_nonnull()   // Option<Box<Data>>
  .unwrap();        // Box<Data>

Single-Pointer Layout for All Types

All xpp smart pointers are sizeof(T*). Rc<T> and Arc<T> point directly to a co-located RcInner { strong, weak, T } block — single allocation, single pointer. std::shared_ptr is two pointers (object + control block), doubling stack/struct footprint.


Comparison with std

Featurexppstd
sizeof (unique)sizeof(T*)sizeof(T*)
sizeof (shared)sizeof(T*)2 × sizeof(T*)
Non-null defaultBox<T>—
Niche OptionYes (nullptr = None)No
Single-thread sharedRc<T> (no atomics)shared_ptr (always atomic)
Thread-safe sharedArc<T>shared_ptr
Custom allocatorYes (Allocator template param, compile-time)std::pmr (type-erased, runtime)
Allocator storageControl block (Arc/Rc) or CompressedPair (Own/Box), EBO when emptyvtable ptr in control block (always)
Deallocation~T() + alloc.deallocate() (separated)deleter(ptr) (single call)
Covariant upcastImplicit (same Allocator)Implicit
Weak observerWeak<T> / ArcWeak<T>weak_ptr<T>
Promise interopNative (.then(), into_nonnull())N/A
Control blockCo-located (single alloc)Separate or intrusive
Header-onlyYesYes