sumty
is a header-only C++20 library that provides "better" sum types. The C++ standard
library provides std::variant
, std::optional
, and std::exepcted
(as of C++23), but
these have some limitiations that sumty
aims to remove.
The C++ language does not provide proper sum types. It does provide union
s, but these
are unsafe and often difficult to use correctly, especially within constexpr
contexts.
The standard library's answer to this deficiency in the language are the generic types
std::variant
, std::optional
, and std::expected
. std::variant
is a general
purpose, type safe, discriminated union. std::optional
is a representation of an object
that either contains some value or contains nothing (null or none). std::exepcted
,
added in C++23, represents an object that either contains an "expected" value or an
"unexpected" value. Although it can be used for other purposes, an "expected" value is
generally a non-error, normal execution value, and an "unexpected" value is generally an
error of some kind, providing an alternative to exceptions.
These types are incredibly useful, but they have a few limitations and miss some
opportunites for optimization. In particular, none of these types allow references
(lvalue or rvalue) to be one of the alternative types. std::variant
and
std::optional
also disallow void
from being an alternative type, but std::expected
does allow void
for the "expected" type. For application authors, these limitations are
usually not an issue since these types either don't come up or have alternate ways to be
represented in non-generic code. For example, T*
can be used instead of
std::optional<T&>
, and bool
can be used instead of std::optional<void>
. But for
library authors, where APIs are much more likely to be generic, these limitations do
become a problem.
If a generic function expects or returns an optional value of any type chosen by the
user, the library either must disallow the same types dissallowed by std::optional
, or
provide and document specializations specifically for references and void
which will
have a slightly different API to suit those types. std::optional
and std::expected
,
in particular are intended for use on API boundaries, so this comes up quite frequently.
For std::variant
, this problem comes up more often in other contexts, such as being
a member of a generic data structure.
Here is a motivating example using std::optional
:
// Calls `fn` if `condition` is `true`, and returns the result. Otherwise, returns null.
template <typename F>
std::optional<std::invoke_result_t<F>> call_if(bool condition, F fn) {
std::optional<std::invoke_result_t<F>> ret;
if (condition) {
ret.emplace(std::invoke(fn));
}
return ret;
}
Without writing additional overloads of call_if
, the provided function must return
a value (cannot return void
). It also cannot return a reference. A minor adjustment
could allow a returned reference to be converted to an rvalue by copying the referenced
value, but there is a strong chance that is not the desired behavior and would be easy to
misuse.
sumty
solves the problem described above by providing sum types that do allow void
and references as alternative types. The previous motivating example can be rewritten as
shown below.
// Calls `fn` if `condition` is `true`, and returns the result. Otherwise, returns null.
template <typename F>
sumty::option<std::invoke_result_t<F>> call_if(bool condition, F fn) {
sumty::option<std::invoke_result_t<F>> ret;
if (condition) {
// `sumty::invoke` cleanly handles the case where `fn` returns `void`
ret.emplace(sumty::invoke(fn));
}
return ret;
}
This new version of call_if
properly handles when fn
returns an lvalue reference, an
rvalue reference, or void
. In addition, the use of sumty::invoke
makes it so
call_if
doesn't even need to specially handle void
internally.
sumty
also takes these sum types a step further with size optimizations. lvalue
references have the particular quality that they are non-null, despite having the same
representation as a pointer. sumty::option<T&>
can use nullptr
to represent "null"
or "none". More generally, any sum type with two alternatives where one alternative is
an lvalue reference and the other is either void
or an empty type, can use nullptr
to represent the non-lvalue reference alternative. Thus, any such sum type is the size of
a pointer.
sumty
also optimizes the size of sum types comprised entirely of void
or empty types.
For example, sumty::option<void>
and sumty::option<std::monostate>
are the same size
as a bool
. More generally, a sum type of all void
or empty type alternatives is the
size of the smallest integer required to count the alternatives.
sumty::variant<void, std::monostate, void>
is the size of a uint8_t
, and
sumty::variant<void>
is an empty type.
Below is a table of the sum types sumty
provides and the standard library type that it
tries to improve upon, if applicable.
sumty |
STL |
---|---|
sumty::variant<T...> |
std::variant<T...> |
sumty::option<T> |
std::optional<T> |
sumty::result<T, E> |
std::expected<T, E> |
sumty::error_set<T...> |
N/A |
variant
is a general purpose discriminated union. It has mostly the same purpose and
API as std::variant
, but there are some important differences aside from expanded
alternative types and size optimizations. Primarily, sumty::variant
does not allow
type-based alternative selection unless such a selection can be unambiguous.
// compiles and initializes the first alternative
std::variant<int, int> v1 = 42;
// does not compile since `42` matches more than one alternative
sumty::variant<int, int> v2 = 42;
This difference is by design to improve readability. In the case of std::variant
, a
less experienced developer may not be sure what should happen in our above example, even
if it is well defined behavior. With sumty::variant
, index based alternative selection
always works and is always clear.
Unlike std::variant
, sumty::variant
also implements the index operator. However, this
operator uses the special compile time indexing types, sumty::index_t
and
sumty::type_t
.
sumty::variant<int, std::string, void> v{"hello"};
auto& s_by_index = v[sumty::index<1>];
auto& s_by_type = v[sumty::type<std::string>];
For the most part, the index operator is an alternative syntax for get
(see docs and
std::get
for std::variant
). But get
throws an exception if the requested
alternative is incorrect, while the index operator is unchecked, resulting in undefined
behavior if the variant
does not contain the requested alternative. std::variant
provides no API for accessing an alternative unchecked.
The last major difference of sumty::variant
from std::variant
is the addition of the
.visit_informed(...)
member function. This function works much like std::visit
, but
it is a member function (and thus only works on one variant
at a time), and it passes
an additional compile time metadata argument to the visitor.
sumty::variant<int, std::string, void> v{"hello"};
v.visit_informed([](auto& value, auto info) {
static constexpr size_t index = info.index;
using type = typename decltype(info)::type;
// ...
});
The info
argument in the example above is an empty type that provides compile time
information describing the index and type of the alternative being visited, which can be
used in if constexpr
conditions, as template arguments, or for any other compile time
use case. Usage of std::visit
on std::variant
often need this information but requires
manual metaprogramming and/or book keeping by the user.
Also note that, unlike the standard library, sumty
implements the "overload pattern" as
a utility. Many consider usage of this pattern a best practice when using std::variant
,
but without sumty
it has to be implemented manually by the user.
sumty::variant<int, std::string, std::vector<int>> v{"hello"};
sumty::visit(sumty::overload {
[](int value) {
// visit `int`
},
[](const std::string& value) {
// visit `std::string`
},
[](const std::vector<int>& value) {
// visit `std::vector<int>`
}
}, v);
Visiting a variant
that contains a void
alternative will pass an instance of void_t
to the visitor for that alternative, instead of passing nothing. This ensures that a
generic parameter can be used in the visitor, even if the alternative might be void
,
rather than having to have a separate overload that expects no argument.
sumty::variant<void, int> v{};
sumty::visit([](auto value) {
// value is either `void_t` or `int`
}, v);
option
is a type that contains "some" value or "none". It should be a drop in
replacement of std::optional
, except that the "some" type can additionally be void
or
a reference (lvalue or rvalue).
sumty::optional<T>
is internally represented as a sumty::variant<void, T>
. As such,
it benefits from all the same size optimizations that the variant
does. option<void>
is the same size as a bool
since it represents no extra information other than being
"some" or "none". An option
of an empty type, such as option<std::monostate>
is also
the size of a bool
, since the empty type need not occupy any extra space. An
option<T&>
is the size of a pointer, since address 0 can be used to represent "none"
while all other addresses signify both "some" and the address value of the reference.
sumty::option
adds a variety of monadic member functions that are not available for
std::optional
under any C++ standard version. Below is a listing of these as well as the
monadic functions that are available for std::optional
, but see the
API docs for a
detailed explanation of each.
Function | Description |
---|---|
.value_or(default) |
Gets the "some" value or returns a default value if "none" |
.value_or_else(fn) |
Gets the "some" value or returns the result of invoking a callable if "none" |
.some_or(default) |
Identical to .value_or(default) |
.some_or_else(fn) |
Identical to .value_or_else(fn) |
.ok_or(error) |
Returns the "some" value as an ok result , or returns the provided error if "none" |
.ok_or_else(fn) |
Returns the "some" value as an ok result , or returns the result of invoking a callable as an error if "none" |
.error_or(ok) |
Returns the "some" value as an error result , or returns the provided ok value if "none" |
.error_or_else(fn) |
Returns the "some" value as an error result , or returns the result of invoking a callable as an ok value if "none" |
.ref() |
Returns an option of a reference to the original "some" value (option<T> -> option<T&> ) |
.cref() |
Returns an option of a const reference to the original "some" value (option<T> -> option<const T&> ) |
.and_then(fn) |
If the option is "some", invokes the callable and returns the result. Otherwise, returns "none" |
.transform(fn) |
Returns a new option containing the result of passing the source "some" value to the callable. The returned option is "none" if the source option was "none". |
.map(fn) |
Identical to .transform(fn) . |
.or_else(fn) |
Returns the original option if it is "some", otherwise returns the result of invoking the callable. |
.flatten() |
Converts an option<option<T>> into option<T> . |
.flatten_all() |
Same as .flatten() but removes all extra layers of option instead of just the first. |
.filter(fn) |
Returns a new option that is "none" if either the original option was "none" or the result of passing the "some" value to the callable is "false". Otherwise returns the original option . |
.transpose() |
Converts an option<result<T, E>> to a result<option<T>, E> . |
sumty
also provides visit
and visit_informed
functions on all sum types. See the
overview of sumty::variant
for details. Additionally, the free
functions sumty::get
and sumty::get_if
work on all sumty
sum types, including
sumty::option
. These functions behave as if the sumty::option<T>
is actually a
sumty::variant<void, T>
.
sumty::option
also works in conjunction with sumty::none_t
and sumty::some<T>(...)
.
none_t
is essentially the same as std::nullopt_t
. An option
can be set from and
compared with an instance of none_t
, sumty::none
. However, std::nullopt
can also be
used for the same purpose. sumty::some<T>(...)
is essentially the same as
std::make_optional<T>(...)
. some<T>(...)
returns an option<T>
with the "some" value
of type T
constructed with the passed arguments.
sumty::option<std::string> get_str_if(bool condition) {
if (condition) {
// returns the string "aaaaa"
return sumty::some<std::string>(5, 'a');
} else {
return sumty::none;
}
}
void test() {
auto opt = get_str_if(true);
assert(opt != sumty::none);
assert(opt != std::nullopt);
opt = sumty::none;
assert(opt == sumty::none);
assert(opt == std::nullopt);
}
sumty::option<T&>
also behaves like non-owning smart pointer. It can be assigned from a
raw pointer, T*
, where a value of nullptr
will set the option to "none", and
otherwise set the value to contain a refrence to the value pointed to by the pointer.
Like with sumty::none_t
and std::nullopt_t
, sumty::option<T&>
can additionally be
assigned from and compared with std::nullptr_t
.
sumty::option<T&> opt{};
assert(opt == nullptr);
int i = 42;
opt = &i;
assert(opt == &i);
result
is a type that contains either an "ok" value or an "error" value. Except that it
does not have any interaction with std::unexpected
, sumty::result
should be a drop in
replacement for std::exepcted
. Instead of std::unexpected
, sumty::result<never, E>
is used. Like with the other sum types of sumty
, both the "ok" type and the "error"
type can be void
or a reference (lvalue or rvalue).
sumty::result<T, E>
is internally represented as a sumty::variant<T, E>
. As such, it
benefits from all the same size optimizations that the variant
does.
result<void, void>
is the same size as a bool
since it represnts no extra information
other than being "ok" or "error". Empty types used for the "ok" or "error" types have the
same effect. If one of the alternatives is an lvalue reference and the other is void
or
an empty type, such as sumty::result<int&, std::monostate>
, the size of the result
is
the same as a pointer.
sumty::result
adds a variety of monadic member functions that are not available for
std::expected
under any C++ standard version. Below is a listing of these as well as
the monadic functions that are available for std::expected
, but see the
API docs for detailed
explanation of each.
Function | Description |
---|---|
.value_or(default) |
Gets the "ok" value or returns a default value if "error" |
.value_or_else(fn) |
Gets the "ok" value or returns the result of invoking a callable if "error" |
.ok_or(default) |
Identical to .value_or(default) |
.ok_or_else(fn) |
Identical to .value_or_else(fn) |
.and_then(fn) |
If the result is "ok", invokes the callable and returns the result. Otherwise, returns "error" |
.transform(fn) |
Returns a new result containing the result of passing the source "ok" value to the callable. The returned result is "error" if the source result was "error". |
.map(fn) |
Identical to .transform(fn) . |
.transform_error(fn) |
Returns a new result containing the result of passing the source "error" value to the callable. The returned result is "ok" if the source result was "ok". |
.map_error(fn) |
Identical to .transform_error(fn) . |
.or_else(fn) |
Returns the original result if it is "ok", otherwise returns the result of invoking the callable. |
.flatten() |
Converts a result<result<T, E>, E> into result<T, E> . |
.flatten_all() |
Same as .flatten() but removes all extra layers of result instead of just the first. |
.transpose() |
Converts a result<option<T>, E> to an option<result<T, E>> . |
.invert() |
Converts a result<T, E> into a result<E, T> . |
.ref() |
Returns a result of a reference to the original "ok" or "error" value (result<T, E> -> result<T&, E&> ) |
.cref() |
Returns a result of a const reference to the original "ok" or "error" value (result<T, E> -> result<const T&, const E&> ) |
.or_none() |
Returns an option containing the "ok" value as a "some" value, or else returns "none" (result<T, E> -> option<T> ) |
.ok_or_none() |
Identical to .or_none() |
.error_or_none() |
Returns an option containing the "error" value as a "some" value, or else returns "none" (result<T, E> -> option<E> ) |
sumty
also provides visit
and visit_informed
functions on all sum types. See the
overview of sumty::variant
for details. Additionally, the free
functions sumty::get
and sumty::get_if
work on all sumty
sum types, including
sumty::result
. These functions behave as if the sumty::result<T, E>
is actually a
sumty::variant<T, E>
.
sumty::result
also works in conjunction with sumty::ok<T>(...)
and
sumty::error<T>(...)
. These functions return an result<T, never>
or
result<never, T>
, respectively. These types can be implicitly converted to a result
of the matching "ok" or "error" type. The arguments passed to these functions passed to
the constructor of the "ok" or "error" type to instantiate the value. However, plain
values also can implicitly convert to an "ok" result, so error<T>(...)
will be used
more frequently than ok<T>(...)
.
sumty::result<int, std::string> get_value_if(bool condition) {
if (condition) {
// return 42; also works
return ok<int>(42);
} else {
return error<std::string>("oh no");
}
}
error_set
is nearly identical to variant
. The most apparent difference is that each
alternative of the set must be unique. A sumty::variant<int, int>
is fine, but a
sumty::error_set<int, int>
would not compile. However, the most important difference,
is an error_set
can be converted to another error_set
whose alternatives are a non-
strict superset of the source. The list below shows some possible conversions. Assume
that each myerr_*
is a distinct type.
error_set<myerr_1> -> error_set<myerr_1, myerr_2>
error_set<myerr_1, myerr_2> -> error_set<myerr_2, myerr_1>
error_set<myerr_1, myerr_3> -> error_set<myerr_3, myerr_2, myerr_1>
myerr_2 -> error_set<myerr_1, myerr_2, myerr_3>
As the name implies, sumty::error_set
is intended to represent a set of possible
errors. Specifically, it is meant to be used as the "error" type of a sumty::result
.
All the above conversion examples also apply to result
types with error_set
"error"
types. For example, sumty::result<int, myerr_1>
converts to
sumty::result<int, sumty::error_set<myerr_1, myerr_2, myerr_3>>
, and
sumty::result<int, sumty::error_set<myerr_1, myerr_3>>
converts to
sumty::result<int, sumty::error_set<myerr_3, myerr_2, myerr_1>>
.
These properties allow for simplified error propagation when using sumty::result
. The
example below shows how these might be used in practice.
// specific error types
struct neg_int_error {};
struct int_parse_error {};
struct int_overflow_error {};
// union of the above error types
using my_errors = sumty::error_set<
neg_int_error,
int_parse_error,
int_overflow_error
>;
// parse an integer from a string
sumty::result<int, int_parse_error> parse_int(std::string_view str);
// calculate the absolute value of an integer
sumty::result<int, int_overflow_error> abs_value(int x);
// calculate the integer square root
sumty::result<int, neg_int_error> isqrt(int x);
sumty::result<int, my_errors> my_algorithm(std::string_view str) {
auto parsed = parse_int(str);
if (parsed.is_error()) { return parsed.prop_error(); }
auto positive = abs_value(*parsed);
if (positive.is_error()) { return positive.prop_error(); }
return isqrt(*positive);
}
The value returned from my_algorithm
is able to describe all the possible errors that
could occur during exuction of the function, but no special effort was required to
to convert the internal errors into the error type returned by my_algorithm
.
Like all other sum types in sumty
, sumty::error_set
has the various size
optimizations for void
, lvalue references, and empty types. However, void
and lvalue
references will not likely be used very frequently. Where error_set
shines is with
empty types. When all the error types of an error_set
are empty types, error_set
becomes a sort of dynamically defined enum
, where each error type designates a value of
the enum
. That is to say, the empty error types need not take up any extra space, so
the error_set
is only the size of the smallest integer needed to count the
error types, which in the vast majority of cases is uint8_t
. But unlike an normal
enum
, error_set
types can be converted to other error_set
types by the
subset/superset rules defined earlier.
Otherwise, sumty::error_set
has an identical API to sumty::variant
.
never
is a type that can never be instantiated without invoking undefined behavior. In
the type theory sense, never
is the empty set type, as there are no possible values an
instance of the type can have. This is different from void
, since void
conceptually
is a unit type, meaning it has one valid value (ignoring language limitations disallowing
variables and members of type void
). A function that returns sumty::never
can never
return, a function that has a sumty::never
argument can never be called, and a struct
or class
with a member of type never
can never be instantiated.
sumty
's sum types optimize specially around alternatives of type never
, and types
that inherit from never
. Since such alternatives can never be instantiated, sum types
in sumty
behave as if the alternative doesn't exit, where possible.
sumty::variant<int, never>
is the same size as an int
. Because the never
alternative can't be instantiated, int
is the only possible alternative. As such, there
isn't even a need for a discriminant, and the variant
can just be a wrapper around the
int
alternative. Similarly, sumty::variant<void, never>
and
sumty::variant<std::monostate, never>
are both empty types.
One of the primary use cases for sumty::never
is as an error type for a
sumty::result
. A result
with a never
error type can only ever be an ok value, so
the result becomes just a wrapper around the ok value. A use case for this might be an
API that requires that a user defined function return a sumty::result
, but not require
any particular error type. If the user's function has no error code paths, they can use
never
as the error type to eek out an extra bit of optimization.
// This is likely to be as optimal as returning `int` after optimization passes
sumty::result<int, never> get_value() {
return 42;
}
sumty::variant<>
and sumty::error_set<>
are also of type sumty::never
.
Specifically, they publically inherit from sumty::never
. So, for example,
sumty::result<int, sumty::error_set<>>
is also just a wrapper around int
. Similarly,
a sum type that is made up entirely of never
alternatives is also a never
type.
Thus, sumty::result<never, never>
, sumty::variant<never, never>
, and
sumty::error_set<never, never>
are also examples of never
types.
Despite being impossbile to instantiate, sumty::never
is still copyable and moveable.
That is, the required constructors and assignment operators for never
to be copyable
and moveable are defined. However, undefined behavior has to be invoked to get to the
point where they are called, and calling them also explicitly invokes undefined behavior.
This ensures that sum types with a never
alternative aren't prevented from being
copyable or moveable by virtue of having such an alternative.
sumty::never
also is conceptually implicitly convertible to any other type. This allows
never
to be used seamlessly within sum types. For example, sumty::result<T, never>
can be implicitly converted to sumty::result<T, E>
, for any error type E
.
The quickest, easiest, and most build system agnostic way to start using sumty
is to
simply add the include
directory of this repo to your include path. Depending on your
compiler, you may also need to enable the C++20 standard, since sumty
requires C++20 or
later.
If your project is CMake based, sumty
can either be pulled in as a subproject, or, if
sumty
is installed, it can be imported using find_package(sumty)
. In either case,
the sumty::sumty
target will be made availble to link into your targets. This will add
the appropriate include directories to your target(s) as well as enforce that the C++20
standard is being used.
Below is an example of using find_package
to import sumty
.
find_package(sumty REQUIRED)
add_executable(app app.cpp)
target_link_libraries(app PRIVATE sumty::sumty)
Below is an example of pulling in sumty
using CMake's FetchContent
feature.
include(FetchContent)
FetchContent_Declare(
sumty
GIT_REPOSITORY https://github.com/Vociferix/sumty
GIT_TAG v0.1.0 # or a commit hash
)
FetchContent_MakeAvailable(sumty)
add_executable(app app.cpp)
target_link_libraries(app PRIVATE sumty::sumty)
Below is an example of using a vendored sumty
repository. Vendoring could mean having
sumty
as a git submodule, or it might be copied into your project.
add_subdirectory(sumty) # use the appropriate path for your project organization
add_executable(app app.cpp)
target_link_libraries(app PRIVATE sumty::sumty)
Some build systems, such as Meson, have built-in support for CMake based dependencies.
In those cases, see the documentation for your build system. If your build system does
not provide CMake support, simply add the sumty
include directory to your include
path, as explained previously.