mp::opt<T, Policy>
is a class template designed to express optionality.
It has interface similar to std::optional<T>
(see here)
but it does not use additional storage space for boolean flag needed to keep information if
the value is set or not. On the other hand type T
should be a type that does not use all
values from its underlying representation thus leaving a possibly to use one of them as a Null
value that express emptiness of type T
.
mp::opt<T, Policy>
library depends on C++14 constexpr features and std::optional<T>
existence so
a fairly modern compiler is needed.
mp::opt<T, Policy>
is a header-only library. Its implementation was split to:
opt.h
that contains the most important implementation and is intended to be included by the user with#include <opt.h>
in the codeopt_bits.h
that contains less important implementation details and is already included byopt.h
header file
Here is a simple example of how to use mp::opt<T, Policy>
for some artificial type price
:
using namespace mp;
using price = int;
using opt_price = opt<price, opt_null_value_policy<price, -1>>;
static_assert(sizeof(opt_price) == sizeof(int));
opt_price o1;
assert(static_cast<bool>(o1) == false);
assert(o1.has_value() == false);
assert(o1.value_or(123) == 123);
constexpr opt_price o2{99};
assert(static_cast<bool>(o2) == true);
assert(o2.has_value() == true);
assert(*o2 == 99);
assert(o2.value() == 99);
assert(o2.value_or(123) == 99);
opt_price o3{o2};
assert(o3.has_value() == true);
assert(*o3 == 99);
o3 = o1;
assert(o3.has_value() == false);
o3 = o2;
assert(o3.has_value() == true);
assert(*o3 == 99);
o3 = nullopt;
assert(o3.has_value() == false);
mp::opt<T, Policy>
uses Policy
class to provide all necessary information needed for proper class operation. The
simplest interface of Policy
class for type my_type
that uses my_type_null_value
as Null value contains
only one member function:
struct my_opt_policy {
static constexpr my_type null_value() noexcept { return my_type_null_value; }
};
mp::opt<my_type, my_opt_policy> my_opt_val;
null_value()
member function returns (by value or reference) special value of typemy_type
that should be used bymp::opt<T, Policy>
as a special Null value to mark emptiness.
mp::opt<T, Policy>
by default uses mp::opt_default_policy<T>
that can be specialized for user's type T
so above
example may be refactored in the following way:
namespace mp {
template<>
struct opt_default_policy<my_type> {
static constexpr my_type null_value() noexcept { return my_type_null_value; }
};
}
mp::opt<my_type> my_opt_val;
The latter solution is suggested for user's types that will always have the same special value used as Null while the former should be used for builtin types like 'int' where the type can express different quantities thus having different Null special values (e.g. age, price, month, etc).
Beside already mentioned mp::opt_default_policy<T>
that can be specialized by the user for his/her type my_type
,
the library provides 2 additional policy types:
-
mp::opt_null_value_policy<T, NullValue>
may be used to provide Null value for integral type right in the class type definition. For example:mp::opt<int, mp::opt_null_value_policy<int, -1>> opt_int;
-
mp::opt_null_type_policy<T, NullType>
may be used for types which values cannot be used as template argument (e.g.float
). That policy type assumes thatNullType::null_value
will represent Null value. For example:struct my_null_float { static constexpr float null_value = 0.0f; }; mp::opt<float, mp::opt_null_type_policy<float, my_null_float>> opt_float;
mp::opt<T, Policy>
needs to compare contained value with special Null value provided by the Policy type. By default
it tries to use operator==()
. If the equality comparison is not provided for my_type
than either:
operator==()
should be provided, or- additional
has_value(const T& value)
member function should be provided in thePolicy
type and it should returnfalse
ifvalue == null_value()
ortrue
otherwise:
struct my_opt_policy {
static constexpr my_type null_value() noexcept { return my_type_null_value; }
static constexpr bool has_value(const T& value) noexcept { return value != null_value(); }
};
mp::opt<my_type, my_opt_policy> my_opt_val;
Sometimes my_type
has some non-trivial logic or validation in its contructors or assignment operators that prevents
one to set special out-of-range value as Null value for the type. In such case it is possible to extend Policy
class with following additional storage_type
public member type. Such storage_type
type:
- should be of the same size as
my_type
type - should be constructible and assignable in the same way as
my_type
- when storing not Null value should have exactly the same memory representation as if the value was stored by
my_type
type - should be used in
null_value()
andhas_value()
member functions instead ofmy_type
.
Below is an example of how opt<bool>
could be implemented:
namepace mp {
template<>
struct opt_default_policy<bool> {
private:
union storage {
bool value;
std::int8_t null_value = -1;
storage() = default;
constexpr storage(bool v) noexcept : value{v} {}
};
public:
using storage_type = storage;
static constexpr storage_type null_value() noexcept { return storage_type{}; }
static constexpr bool has_value(storage_type s) noexcept { return s.null_value != -1; }
};
}
Here is another a bit more complicated example for some (not too smart ;-) ) weekday
class:
class weekday {
public:
using underlying_type = std::int8_t;
constexpr explicit weekday(underlying_type v) : value_{v}
{
if(v < 0 || v > 6) throw std::out_of_range{"weekday value outside of allowed range"};
}
constexpr weekday& operator=(underlying_type v)
{
if(v < 0 || v > 6) throw std::out_of_range{"weekday value outside of allowed range"};
value_ = v;
return *this;
}
constexpr underlying_type get() const noexcept { return value_; }
private:
underlying_type value_; // 0 - 6
};
constexpr bool operator==(weekday lhs, weekday rhs) noexcept { return lhs.get() == rhs.get(); }
constexpr bool operator==(weekday::underlying_type lhs, weekday rhs) noexcept { return lhs == rhs.get(); }
constexpr bool operator==(weekday lhs, weekday::underlying_type rhs) noexcept { return lhs.get() == rhs; }
namespace mp {
template<>
struct opt_default_policy<weekday> {
union storage_type {
weekday value;
weekday::underlying_type null_value = std::numeric_limits<weekday::underlying_type>::max();
constexpr storage_type() noexcept {};
constexpr storage_type(weekday v) : value{v} {}
constexpr storage_type(weekday::underlying_type v) : value{v} {}
storage_type& operator=(weekday v)
{
value = v;
return *this;
}
};
public:
static storage_type null_value() noexcept { return storage_type{}; }
static bool has_value(storage_type value) noexcept
{
return value.null_value != std::numeric_limits<weekday::underlying_type>::max();
}
};
}