Replacement for std::optional
with efficient memory usage and additional features.
- Functionality from C++23
std::optional
, Rust'sstd::option::Option
and otheropt::option
's own stuff. See reference. - Zero memory overhead with types that have unused values. See builtin traits.
- Support for nested
opt::option
s with zero memory overhead. - Simpler interface than
std::optional
(constructors withoutstd::in_place
), supports construction of aggregate types in C++17 (using direct-list-initialization for them). - Custom size optimizations for your own types (
opt::option_traits
). See option traits guide. - Allows reference types.
Table of contents:
- Overview
- Why
opt::option
? - Additional functionality
- Compiler support
- CMake integration
- How it works
- Compatibility with
std::optional
- About undefined behavior
- Build times
- Examples
Use #include <opt/option.hpp>
to include the library header file.
The contents of the library are available in the opt
namespace.
Types with unused states.
opt::option<float> a = 123.456f;
// Has the same size as just float
static_assert(sizeof(a) == sizeof(float));
// Convert `opt::option<float>` into `opt::option<int>`, and print the value if it is not empty
a.map([](float x) { return int(x); }).inspect([](int x) {
std::cout << x << '\n';
});
a = opt::none;
// Convert `opt::option<float>` to `opt::option<int>` if it contains a value; otherwise, invoke the 'else' condition.
int b = a.map_or_else([] { return 1; }, [](float x) { return int(x) + 5; });
std::cout << b << '\n';
int c = 2;
opt::option<int*> d = &c;
// Has the same size as just a pointer
static_assert(sizeof(d) == sizeof(int*));
// Print the dereferenced value
std::cout << **d << '\n';
// Empty opt::option is not nullptr for pointers!
d = nullptr;
// Print the pointer address or "empty option" if option does not contain one
std::cout << opt::io(d, "empty option") << '\n';
Complex types that contain unused states.
opt::option<std::tuple<int, unsigned, float>> a;
// Uses `float` value in `std::tuple` to store "has value" state
static_assert(sizeof(a) == sizeof(std::tuple<int, unsigned, float>));
a.emplace(1, 2u, 3.f);
std::cout << std::get<0>(*a) << std::get<1>(*a) << std::get<2>(*a) << '\n';
struct S1 {
unsigned x;
char y;
bool z;
};
opt::option<S1> b{5u, 'a', false};
// Uses `bool` value in `S1` to store "has value" state
static_assert(sizeof(b) == sizeof(S1));
b.reset();
std::cout << b.has_value() << '\n';
struct S2 {
S1 x;
std::tuple<int, int> y;
};
opt::option<S2> c{S2{S1{1, 'b', true}, {2, 3}}};
// Uses `bool` value in `x` data member inside `S1` type to store "has value" state
static_assert(sizeof(c) == sizeof(S2));
c->x.x = 100u;
std::cout << c->x.x << '\n';
Nested opt::option
s.
opt::option<opt::option<bool>> a{true};
// Uses `bool` value to store two "has value" states
static_assert(sizeof(a) == sizeof(bool));
a->reset();
a.reset();
opt::option<opt::option<opt::option<opt::option<opt::option<float>>>>> b;
// Uses `bool` value to store emptiness level value
static_assert(sizeof(b) == sizeof(float));
(*****b).reset();
(****b).reset();
(***b).reset();
(**b).reset();
b->reset();
b.reset();
opt::option
allows to minimize the type size to a minimum.
Minimizing the type size is always a good thing if used properly.
Cache locality can often improve performance of the program even more than any other performed optimization.
It supports reference types, so you can avoid using inconvenient std::reference_wrapper
and dangerous nullable pointers.
Allows direct-list-initialization for an aggregate types and constructors without std::in_place
(but they are still supported).
Features taken from Rust's std::option::Option
(.take
, .map_or(_else)
, .flatten
, .unzip
, etc.), monadic operations from C++23 (.and_then
, .map
(renamed .transform
), .or_else
) and custom ones (.ptr_or_null
, opt::option_cast
, opt::from_nullable
, operators equivalent to methods, etc.).
Extended constexpr
support for trivially move assignable types.
Note that size optimizations prevent opt::option
being constexpr
-compatible.
The option library provides extended functionality over standard std::optional
, which can lead to the use of more efficient and cleaner code.
See feature list for a list of available features.
See reference for more details.
The library is tested with these compiler versions with sanitizers enabled1:
- GCC: 14.0.1, 13.2.0, 12.3.0, 11.4.0
- Clang: 18.1.3, 17.0.6, 16.0.6, 15.0.7, 14.0.0, 13.0.1, 12.0.1, 11.1.0, 10.0.0, 9.0.1 (
libc++
andlibstdc++
) - MSVC: 19.40.33813.0 (VS v143)
- ClangCL: 17.0.3
- IntelLLVM: 2024.2.1
Important
The library could work with other versions of compilers, but some functionality may be broken.
Consider using sanitizers when using library using an untested version of the compiler to prevent unexpected behaviour.
The library has various tests that tries to cover every part of the library.
Uses clang-tidy (18.1.8) to minimize the number of bugs and unexpected behavior.
The CMakeLists.txt
file in the project root directory provides option
INTERFACE
target, which adds:
- include directory
include/
. cxx_std_17
compile feature.- Includes
debugger/option.natvis
anddebugger/option.natstepfilter
with ability to disable them.
See project cmake variables for more information.
To use find_package
command you need to firstly install the option
project.
Generate the option
project with the variable OPTION_INSTALL
defined to TRUE
:
cmake -B build -DOPTION_INSTALL=TRUE
Generate project into
build
directory, and allow to install it.
sudo cmake --install build
Install project from
build
directory. Requires administrative permissions
Note
The version file for the project has the COMPATIBILITY
mode set to ExactVersion
and ARCH_INDEPENDENT
argument.
Use find_package
to find the option
library and target_link_libraries
to specify dependency on it.
find_package(option REQUIRED)
...
target_link_libraries(<target> PRIVATE option)
Next, you can #include
the library header to use it.
You can use FetchContent
either to clone the git repository or specify the archive URL to directly download it.
Download through git repository:
include(FetchContent)
FetcnhContet_Declare(
option
GIT_REPOSITORY https://github.com/NUCLEAR-BOMB/option.git
GIT_TAG <commit/tag>
)
FetchContent_MakeAvailable(option)
...
target_link_libraries(<target> PRIVATE option)
Tip
You could specify the SYSTEM
(since CMake 3.25) and EXCLUDE_FROM_ALL
(since CMake 3.28) arguments to FetchContent_Declare
but the library already uses target_include_directories
with SYSTEM
and it is header-only library.
Using URL to the archive:
include(FetchContent)
FetchContent_Declare(
option
URL https://github.com/NUCLEAR-BOMB/option/archive/<id>
URL_HASH SHA256=<hash>
)
FetchContent_MakeAvailable(option)
...
target_link_libraries(<target> PRIVATE option)
You can create archive URL with Source code archive URLs.
<hash>
is optional but recommended. With this integrity check you can more secure pin the library version and avoid possible data corruptions and "changed file in transit" scenarios.
Tip
Use FIND_PACKAGE_ARGS
optional argument in FetchContent_Declare
to make it firstly try a call to find_package
.
Download through git repository:
include(ExternalProject)
ExternalProject_Add(
option
PREFIX "${CMAKE_BINARY_DIR}/option"
GIT_REPOSITORY https://github.com/NUCLEAR-BOMB/option.git
GIT_TAG <commit/tag>
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
TEST_COMMAND ""
)
ExternalProject_Get_Property(option source_dir)
target_compile_features(<target> PRIVATE cxx_std_17)
target_include_directories(<target> SYSTEM PRIVATE "${source_dir}/include")
As it is a header-only library it doesn't requires building, configuring, installation or testing.
If you want to run test target before library is used:
include(ExternalProject)
ExternalProject_Add(
option
PREFIX "${CMAKE_BINARY_DIR}/option"
GIT_REPOSITORY https://github.com/NUCLEAR-BOMB/option.git
GIT_TAG <commit/tag>
TIMEOUT 10
INSTALL_COMMAND ""
TEST_COMMAND ${CMAKE_COMMAND} --build . --target run-option-test
)
This will build the tests and run them with
run-option-test
target. Note that this may take some time to build tests.
Note
You can change the download method to download through URL instead of through git repository with URL
and URL_HASH
arguments.
You can directly embed the project and add it through CMake's add_subdirectory
command.
add_subdirectory(<path>)
target_link_libraries(<target> PRIVATE option)
<path>
is path to the root directory of the project (containsCMakeLists.txt
).
The opt::option
internally uses opt::option_traits
which contains static methods that manipulates underlying value inside opt::option
.
That's provide a way to store an empty state in a opt::option
without using additional bool
flag variable.
opt::option_traits
also defines recursively opt::option
type optimization and allows nested opt::option
to have same size as the contained value.
Quick list of built-in size optimizations:
bool
: sincebool
only usesfalse
andtrue
values, the remaining ones are used.- References and
std::reference_wrapper
: around zero values are used. - Pointers: for x64 noncanonical addresses, for x32 slightly less than maximum address.
- Floating point: negative signaling NaN with some payload values are used (quiet NaN is available).
- Polymorphic types: unused vtable pointer values are used.
- Reflectable types (aggregate types)2: the member with maximum number of unused value are used.
- Pointers to members (
T U::*
): some special offset range is used. std::tuple
,std::pair
,std::array
and any other tuple-like type: the member with maximum number of unused value are used.std::basic_string_view
andstd::unique_ptr<T, std::default_delete<T>>
: special values are used.std::basic_string
andstd::vector
: uses internal implementation of the containers (supportslibc++
,libstdc++
andMSVC STL
).- Enumeration reflection3: automatic finds unused values (empty enums and flag enums are taken into account).
- Manual reflection: sentinel non-static data member (
.SENTINEL
), enumeration sentinel (::SENTINEL
,::SENTINEL_START
,::SENTINEL_END
). opt::sentinel
,opt::sentinel_f
,opt::member
: user-defined unused values.
See built-in traits for more information.
The library is fully compatible with std::optional
4, except:
std::optional<T>::transform
is calledopt::option<T>::map
.- Size of
std::optional
is not always the same asopt::option
. - Some operations on types are not always
constexpr
depending on the option traits. std::bad_optional_access
isopt::bad_access
.std::nullopt
/std::nullopt_t
isopt::none
/opt::none_t
.
You can replace std::optional
with opt::option
, taking into account that there are these exceptions.
The library actively uses platform-dependent behavior to exploit unused object states.
Recommended using sanitizers (AddressSanitizer
and UndefinedBehaviorSanitizer
) to catch unexpected behavior.
Note
The library doesn't break the strict aliasing rules. It uses std::memcpy
to copy object bits instead of reinterpret_cast
.
You can disable individual built-in traits to avoid using platform specific behavior for specific types. Or you can disable built-in traits entirely with a macro definition.
The opt::option
is slightly slower to build than std::optional
due to meta-programming overhead and addutional functionality.
The benchmarks are created with CMake's custom command which creates a file with 2000 instantiations of opt::option
/std::optional
using a Python script.
Benchmarks are performed on commit 0e882312c4451ac937103eb8830531ee2726f863
.
Compiler (version, (stdlib), platform) | opt::option Debug |
std::optional Debug |
opt::option Release |
std::optional Release |
---|---|---|---|---|
MSVC (19.40.33811 , x64) |
09:774 | 06:143 | 10:552 | 06:100 |
Clang (18.1.8 , libstdc++ , x64) |
14:958 | 12:757 | 12:829 | 11:140 |
Clang (18.1.8 , libc++ , x64) |
13:203 | 08:418 | 12:085 | 07:168 |
GCC (14.2.0 , libstdc++ , x64) |
10:793 | 10:964 | 07:783 | 07:849 |
Note
The Clang and GCC are used on WSL.
Time is in seconds (e.g. 123:456
is 123 seconds and 456 milliseconds).
On average, compiling opt::option
takes ~1.33x longer than std::optional
(on Debug
configuration).
You can find examples in the examples/
directory.
functions.cpp
: functions that are defined in theopt
namespace.methods.cpp
: unique methods for the library.operators.cpp
: unique operators for the library.option_traits.cpp
: creating custom option trait.
The output of all the examples is checked using script at examples/check.py
.
Footnotes
-
When possible uses Address Sanitizers and Undefined Behavior Sanitizer. Note that some compilers/versions have unstable sanitizer support, so the CI tests are disables that options. ↩
-
Requires identifier
__PRETTY_FUNCTION__
or compiler built-in__builtin_FUNCSIG()
. ↩ -
Including: conditionally enabled constructors and methods, propagating trivial constructors and operators, propagating deleted constructors, operators. ↩