A header-only C++23 library. Drop-in optional that uses sentinel values instead of a bool flag — same API as std::optional, half the memory.
slim::optional only works with types that have a sentinel value defined via sentinel_traits. For types without a natural sentinel (e.g., bool, char, aggregates), use std::optional.
std::optional<int*> 16 bytes slim::optional<int*> 8 bytes
std::optional<int> 8 bytes slim::optional<int> 4 bytes
std::optional<double> 16 bytes slim::optional<double> 8 bytes
Copy the header into your project:
cp include/slim/optional.hpp your_project/include/slim/
Or add as a CMake subdirectory:
add_subdirectory(slim-optional) # path to this repo
target_link_libraries(myapp PRIVATE slim_optional)Either way:
#include <slim/optional.hpp>
int x = 42;
slim::optional<int*> ptr = &x; // 8 bytes, not 16
if (ptr) std::cout << **ptr; // 42
ptr.reset(); // emptyRequires GCC 13+ or Clang 17+.
The full std::optional interface — constructors, value(), value_or(), operator*, operator->, reset(), emplace(), swap(), and the C++23 monadic operations (and_then, transform, or_else). Plus CTAD:
slim::optional opt(42); // deduces optional<int>
auto opt2 = slim::make_optional(&x);These work out of the box via built-in sentinel_traits specializations:
Always available:
| Type | Sentinel value |
|---|---|
Signed integers (int, int64_t, ...) |
numeric_limits::min() |
Unsigned integers (unsigned, size_t, ...) |
numeric_limits::max() |
float, double, long double |
NaN (any) |
Pointers (T*) |
nullptr |
char16_t, char32_t |
0xFFFF, 0xFFFFFFFF |
exception_ptr |
null |
Requires full mode (disabled by SLIM_OPTIONAL_LEAN_AND_MEAN):
| Type | Sentinel value |
|---|---|
unique_ptr<T>, shared_ptr<T> |
null |
string_view, span<T> |
null data pointer |
function<F>, move_only_function<F> |
empty |
coroutine_handle<P> |
null handle |
any |
empty |
thread::id |
default-constructed |
stop_token |
non-stoppable |
chrono::duration<Rep>, chrono::time_point<C,D> |
underlying sentinel |
complex<T> |
NaN |
Specialize sentinel_traits in namespace slim to opt in your own types:
enum class FileHandle : int { INVALID = -1, STDIN = 0, STDOUT = 1 };
namespace slim {
template<>
struct sentinel_traits<FileHandle> {
static constexpr FileHandle sentinel() noexcept { return FileHandle::INVALID; }
static constexpr bool is_sentinel(const FileHandle& v) noexcept {
return v == FileHandle::INVALID;
}
};
}
slim::optional<FileHandle> fd; // 4 bytes (std::optional<FileHandle> = 8)
fd = FileHandle::STDOUT;Construct from, convert to, and compare with std::optional. The and_then monadic operation accepts callbacks returning either slim::optional or std::optional.
std::optional<int*> std_opt = &x;
slim::optional<int*> slim_opt(std_opt); // construct from std::optional
std::optional<int*> back = slim_opt; // implicit conversion back
assert(slim_opt == std_opt); // comparison works
// and_then works with std::optional return type
auto result = slim_opt.and_then([](int* p) -> std::optional<int> {
return *p * 2;
});std::numeric_limits<slim::optional<T>> is specialized for numeric types and reflects the reduced valid range:
// Signed: min() is the sentinel, so valid min is one higher
std::numeric_limits<slim::optional<int>>::min() // INT_MIN + 1
std::numeric_limits<slim::optional<int>>::lowest() // INT_MIN + 1
std::numeric_limits<slim::optional<int>>::max() // INT_MAX (unchanged)
// Unsigned: max() is the sentinel, so valid max is one lower
std::numeric_limits<slim::optional<unsigned>>::max() // UINT_MAX - 1
// Float: NaN is the sentinel, so NaN is no longer available
std::numeric_limits<slim::optional<float>>::has_quiet_NaN // false
std::numeric_limits<slim::optional<float>>::has_signaling_NaN // falseUnlike std::optional, which uses a union and only constructs T when engaged, slim::optional always holds a live T object — either the real value or the sentinel. This gives it a simpler lifetime model:
- No placement-new or explicit destructor calls. All state transitions use assignment.
reset()assigns the sentinel;emplace()move-assigns;operator=assigns. - No engaged/empty state matrix.
std::optional::operator=has four cases (engaged/empty x engaged/empty), each with different construct/destroy/assign behavior.slim::optionalalways assigns.
This is safe for types with trivial constructors and destructors (integers, floats, pointers, char16_t/char32_t) — constructing an int with INT_MIN or destroying one is a no-op, so the always-alive model produces identical codegen to std::optional.
For unique_ptr and shared_ptr, the sentinel is nullptr — the same state as a default-constructed smart pointer. Assigning nullptr releases the owned resource just as destruction would, so reset() and operator=(nullopt) correctly free memory. An empty slim::optional<unique_ptr<T>> simply holds a null unique_ptr, which is a zero-cost, well-defined state.
For a detailed comparison of constructor/destructor semantics against std::optional, see ANALYSIS.md.
Storing the sentinel value directly is a programming error and throws slim::bad_optional_access. Use reset() or nullopt to clear.
slim::optional<int*> opt(&x);
opt = nullptr; // throws slim::bad_optional_access
opt.reset(); // OK
opt = slim::nullopt; // OKDefine SLIM_OPTIONAL_LEAN_AND_MEAN to minimize standard library includes and disable sentinel_traits for std library types. Core types (integers, floats, pointers, char16_t/char32_t, exception_ptr), custom user types, and std::optional interop all still work.
# CMake
cmake -B build -DSLIM_OPTIONAL_LEAN_AND_MEAN=ON
# Or define directly
target_compile_definitions(myapp PRIVATE SLIM_OPTIONAL_LEAN_AND_MEAN)cmake -B build
cmake --build build
ctest --test-dir build # run tests
./build/examples/examples # run examples
./build/benchmarks/perf_bench # run benchmarksMIT