Skip to content

C++20 optional type without size overhead

License

Notifications You must be signed in to change notification settings

s9w/intrusive_optional

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

48 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

intrusive_optional

The C++ standard library has a std::optional<T> type which either contains a T with some value or is empty. Such an optional type must always have a sizeof bigger than T. At least by one byte, but usually more because of alignment constraints.

In most applications, that's not an issue. There are situations however where you really want an optional without size overhead - intrusive_optional is a C++20 library that does just that. That is only possible if we give up one possible state of T which marks the null state - the equivalent of std::nullopt in an std::optional.

intrusive_optional is instantiated with its null value and otherwise completely mirrors the interface of std::optional, see below for minor differences. Since the null value is part of the type, it needs to be literal, i.e. instantiable at compile-time. Examples:

#include "intrusive_optional.h"

using optional_double = io::intrusive_optional<std::numeric_limits<double>::max()>;
static_assert(sizeof(optional_double) == sizeof(double));

constexpr optional_double default_value;
static_assert(default_value.has_value() == false);

constexpr optional_double assigned_value = 5.0;
static_assert(assigned_value.has_value() == true);
static_assert(assigned_value.value() == 5.0);

The whole thing just a single header so grab that from GitHub.

⚠️ This library solves a very special problem and comes with the constraints stated above. It's not recommended for general replacement of std::optional<T> or alternatives like swl::optional. Please don't hurt yourself with this.

Safety mode

There's an apparent drawback to implementing an optional like this: The replicated interface of std::optional has several functions that offer direct access to the underlying value (e.g. operator*). That access can be used to set the stored value to its null value. That's probably unintentional as there's .reset() and operator=(std::nullopt_t) and can be a source of bugs.

To prevent this, intrusive_optional has an optional second template parameter that can be used to enable a special safety mode. It removes dangerous functions which can't be checked for unintentional nulling and throws an exception where such checks are possible and triggered.

Safety mode can be enabled like this:

using safe_int_optional = io::intrusive_optional<-1, io::safety_mode::safe>

In detail, safety mode does this:

  • Constructors (6), (7) and (8) throw io::unintentionally_null when resulting in the null value
  • operator= overload (4) throw io::unintentionally_null when resulting in the null value
  • All emplace overloads throw io::unintentionally_null when resulting in the null value
  • Non-const overloads of operator*() and value() are disabled

By default the safety mode is disabled so you can ignore that if you prefer.

Conversion from and to std::optional

Conversion to std::optional is provided by the function constexpr auto get_std() const -> std::optional<value_type>.

Converting from std::optional to an intrusive_optional can either be done by constructor or assignment.

std::optional<int> std_opt(42);

// Conversion from std::optional via construction
io::intrusive_optional<-1> tight_optional(std_opt);

// Conversion from std::optional via assignment
tight_optional = std_opt;

Compatibility with std::optional

The first overload of std::make_optional is such that you can write std::make_optional(5) and the type (here: int) will be deduced automatically. That isn't possible with intrusive_optional since its instantiation requires a value and not just a type. Hence that overload is removed. You can still use the other overloads like std::make_optional<my_type>(3).

Also comparison (33) isn't implemented. That's a three-way comparison between an optional and a value where the value_type of the optional and the other parameter are comparable with each other. This fails due to compile errors, hopefully fixed in future versions.

Motivation

My original motivation was building a concurrency type that was based on std::atomic<std::optional<T>>. Atomics are crucially size-limited, only resolving to fast code paths for types of 8 bytes or less. Using that with an 8-byte type like std::chrono::time_point isn't possible. The other problem is that std::atomic<T>::wait() uses bitwise comparison and not operator==. But two std::optional types are not bitwise-equal if they're both nullopt.

intrusive_optional solves both these problems. Since the entire state is encoded in a single T, bitwise comparison works and there's no size overhead.

TODO

  • or_else etc, c++23 interface

About

C++20 optional type without size overhead

Topics

Resources

License

Stars

Watchers

Forks

Languages