Awesome
<p align="center"> <img src="docs/img/logo.png" width="300"> </p>
<a href=""><img src="docs/img/C++17.svg" alt="C++17"></a> <a href=""><img src="docs/img/include_option_hpp.svg" alt="#include <opt/option.hpp>"></a>
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
Overview
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();
Why opt::option
?
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.
Additional functionality
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.
Compiler support
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.
CMake integration
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.
find_package
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 toExactVersion
andARCH_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.
FetchContent
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) andEXCLUDE_FROM_ALL
(since CMake 3.28) arguments toFetchContent_Declare
but the library already usestarget_include_directories
withSYSTEM
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 inFetchContent_Declare
to make it firstly try a call tofind_package
.
ExternalProject
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
andURL_HASH
arguments.
add_subdirectory
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
).
How it works
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.
Compatibility with std::optional
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.
About undefined behavior
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 ofreinterpret_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.
Build times
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 <br/> Debug | std::optional <br/> Debug | opt::option <br/> Release | std::optional <br/> 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).
Examples
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. ↩