An additive strong typedef library for C++14/17/20 using the Boost Software License 1.0
Very much inspired by @foonathan's
type_safe
library, but aim is
slightly different. Limit scope for type safety only. No runtime checks. Also
strive for a higher level abstraction of the needed functionality. The idea
is to suffer no runtime penalty, but to capture misuse at compile time
(for example accidentally subtracting from a handle, or swapping two parameters
in a function call) while still being easy to use for inexperienced
programmers.
Example use:
#include <strong_type/strong_type.hpp>
using myint = strong::type<int, struct my_int_>;
myint
is a very basic handle. You can initialize it. You can do
equal/not-equal comparison with other instances of the same type, and you can
access its underlying int
instance with value_of(variable)
.
To get the underlying type of a strong type, use
typename strong::underlying_type<mytype>::type
, or the convenience alias
strong::underlying_type_t<mytype>
. If mytype
is not a strong::type
,
they give mytype
.
using otherint = strong::type<int, struct other_int_>;
otherint
is a distinct type from myint
. If a function takes an argument of
type myint
, you can't pass it an instance of otherint
, and vice-versa. You
also can't cross-assign, cross-create or cross-compare.
To access more functionality, you add modifiers. For example:
using ordered_int = strong::type<int, struct ordered_int_, strong::ordered>;
Type ordered_int
now supports relational order comparisons, like <
,
(provided the underlying type, int this case int
, does.) Type ordered_int
can thus be used as key in std::map
<> or std::set<>
.
The header file <strong_type/strong_type.hpp>
brings you all functionality.
There are more fine-grained headers available, which may speed up builds in
some situations.
A strong type can be used as an NTTP (Non Type Template Parameter), if the underlying type can be, for compilers and standards that support it.
-
strong::affine_point<D>
allows instances to be subtracted (yielding aD
) or to add or subtract aD
to an instance. See Affine Space. Examples of one dimentional affine points are pointer (withD
beingptrdiff_t
,) orstd::time_point<>
(withstd::duration<>
asD
.) An example of a multidimensional affine point is a coordinate (with a vector type forD
.)D
can be defaulted, usingstrong::affine_point<>
, in which case the difference type shares the same tag. The difference type from astrong::affine_point<D>
can be obtained usingtype::difference
, regardless of whetherD
is explicit or defaulted. It is natural thatD
is of astrong::difference
type. This is a good name from a mathematical point of view, but perhaps a bit too academic, and not well aligned with the other names.Available in
strong_type//affine_point.hpp
. -
std::numeric_limits<T>
is specialized for types using thestrong::arithmetic
modifier.Available in
strong_type/arithmetic.hpp
. -
strong::bicrementable
. Obviously a made up word for the occation. Implements bothstrong::incrementable
andstrong::decrementable
.Available in
strong_type/bicrementable.hpp
-
strong::bitarithmetic
allows bitwise&
, bitwise|
, bitwise^
and shift operations.Available in
strong_type/bitarithmetic.hpp
. -
strong::boolean
providesexplicit operator bool() const
, providing the underlying type supports it.Available in
strong_type/boolean.hpp
. -
Available in
strong_type/convertible_to.hpp
. -
strong::decrementable
. Providesoperator--
for the strong type, using the operator of the underlying type.Available in
strong_type/incrementable.hpp
-
Available in
strong_type/type.hpp
-
strong::difference
allows instances to be subtracted and added (yielding astrong::difference
).Conditionally, if the underlying type supports it,
strong_difference
is ordered, may be divided (yielding the base type), or multiplied or divided with the base type, yielding anotherstrong::difference
. Also, conditionally, the remainder after division of two differences yields the underlying type, and the remainder after division of a difference and the underlying type yields a difference. Astrong::difference
is alsostrong::equality
.Available in
strong_type/difference.hpp
. -
Available in
strong_type/equality.hpp
. -
strong::equality_with<Ts...>
provides operators==
and!=
between the strong type and each of the typesTs...
. Note! WhileTs
can include other strong types, it can not refer to the strong type being defined. Usestrong::equality
for that.Available in
strong_type/equality_with.hpp
. -
strong::formattable
addsstd::format
and/orfmt::format
capability, based on availability of the formatting library. This can further be controlled (globally) with the definesSTRONG_HAS_STD_FORMAT
respectivelySTRONG_HAS_FMT_FORMAT
. With 0 to disable the support completly, and with 1 to force the support, disable the auto detection.fmt::format
allows formatting also types that arestrong::ostreamable
.Available in
strong_type/formattable.hpp
. -
Available in
strong_type/hashable.hpp
. -
Available in
strong_type/implicitly_convertible_to.hpp
. -
strong::incrementable
. Provides `operator++ for the strong type, using the operator of the underlying type.Available in
strong_type/incrementable.hpp
-
Available in
strong_type/indexed.hpp
. -
Available in
strong_type/invocable.hpp
-
strong::iostreamable
. Bothstrong::istreamable
andstrong::ostreamable
.Available in
strong_type/iostreamable.hpp
-
strong::istreamable
. Provides the defaultistream
extractionoperator>>
for the strong type, as handled by the underfying type. Provide your own operator istead if you prefer a custom istream extraction operator.Available in
strong_type/istreamable.hpp
-
strong::iterator
adds functionality needed depending on iterator category. If the iterator type is arandom_access_iterator
, the strong type isstrong::indexed<>
andstrong::affine_point<difference>
. It should be possible to specify the index type and affine_point type.The type trait
std::iterator_traits
mirrors the traits of the underlying iterator type.Available in
strong_type/iterator.hpp
-
Available in
strong_type/ordered.hpp
-
strong::ordered_with<Ts...>
provides operators '<', '<=', '>=' and '>' between the strong type and each of the typesTs...
. Note! WhileTs
can include other strong types, it cannot refer to the strong type being defined. Usestrong::ordered
for that.Available in
strong_type/ordered_with.hpp
-
strong::ostreamable
. Provides the defaultostream
insertionoperator<<
for the strong type, as handled by the underlying type. Provide your own operator instead if you prefer a custom ostream insertion operator.Available in
strong_type/ostreamable.hpp
-
strong::partially_ordered
provides operator '<=>' The strong type offers the same ordering relatin as the underlying type. The result isstd::partial_ordering
. Note! This does not imply ´strong::equality´.Available in
strong_type/ordered.hpp
-
strong::partially_ordered_with<Ts...>
provides operator '<=>' between the strong type and each of the typesTs...
. Note! WhileTs
can include other strong types, it cannot refer to the strong type being defined. Usestrong::partially_ordered
for that. The result isstd::partial_ordering
. Note! This does not imply ´strong::equality_with<Ts...>´.Available in
strong_type/ordered_with.hpp
-
Available in
strong_type/pointer.hpp
-
strong::range
adds the functionality needed to iterate over the elements. The iterator types are using the same tag as using in the range. Only implements typesiterator
andconst_iterator
, and thus.begin()
,.end()
,.cbegin()
,.cend()
,.begin() const
and.end() const
. The member function.size() const
is conditionally supported if the underlying range type supports it.Available in
strong_type/range.hpp
-
strong::regular
. Same assemiregular
and also equality comparable. A good default base for most types.Available in
strong_type/regular.hpp
-
Available in
strong_type/scalable_with.hpp
-
Available in
strong_type/semiregular.hpp
. -
strong::strongly_ordered
provides operator '<=>' The strong type offers the same ordering relatin as the underlying type. The result isstd::strong_ordering
. Note! This does not imply ´strong::equality<Ts...>´.Available in
strong_type/ordered.hpp
-
strong::strongly_ordered_with<Ts...>
provides operator '<=>' between the strong type and each of the typesTs...
. Note! WhileTs
can include other strong types, it cannot refer to the strong type being defined. Usestrong::strongly_ordered
for that. The result isstd::strong_ordering
Note! This does not imply ´strong::equality_with<Ts...>´.Available in
strong_type/ordered_with.hpp
-
Available in
strong_type/unique.hpp
-
strong::weakly_ordered
provides operator '<=>' The strong type offers the same ordering relatin as the underlying type. The result isstd::weak_ordering
. Note! This does not imply ´strong::equality´.Available in
strong_type/ordered.hpp
-
strong::weakly_ordered_with<Ts...>
provides operator '<=>' between the strong type and each of the typesTs...
. Note! WhileTs
can include other strong types, it cannot refer to the strong type being defined. Usestrong::weakly_ordered
for that. The result isstd::weak_ordering
Note! This does not imply ´strong::equality_with<Ts...>´.Available in
strong_type/ordered_with.hpp
A number of small utilities are available directly in strong_type/type.hpp
.
-
strong::type
provides a non-memberswap()
function as a friend, which swaps underlying values using. -
strong::underlying_type<Type>
isT
forstrong::type<T, Tag, Ms...>
and public descendants, andType
for other types. -
strong::uninitialized
can be used to construct instances ofstrong::type<T...>
without initializing the value. This is only possible if the underlying type istrivially default constructible
, for example:void init(int*); void function() { strong::type<int, struct int_tag> x(strong::uninitialized); // x will have an unspecified value init(&value_of(x)); // hopefully the init() function assigns a value }
-
strong::type_is<type, modifier>
, a boolean constant type whith the value ofstrong::type_is_v<type, modifier>
. -
strong::type_is_v<type, modifier>
is a constexpr predicate to test if a type has a modifier. For variadic modifiers, likestrong::ordered_with<Ts...>
, it tests each of the typesTs
individually. Example:using handle = strong::type<int, struct handle_, strong::regular>; static_assert(strong::type_is_v<handle, strong::equality>); static_assert(strong::type_is_v<handle, strong::regular>); using id = strong::type<int, struct id_, strong::ordered_with<int, long>>; static_assert(strong::type_is_v<id, strong::ordered_with<int, long>>); static_assert(strong::type_is_v<id, strong::ordered_with<long>>); static_assert(strong::type_is_v<id, strong::ordered_with<int>>); static_assert(strong::type_is_v<id, strong::ordered_with<>>);
All
static_assert
s above pass.
A modifier is a nested structure. The outer type, a struct or class, is what
the user sees. Inside it is a struct/class template that is a
CRTP mixin, and
it must be named modifier
, and the type it will be instantiated with is the
complete strong type. A type
using my_strong_type = strong::type<int, struct my_, my_modifier>
will inherit
publically from my_modifier::modifier<my_strong_type>
. This CRTP mixin
implements the functionality of the modifier.
As an example, let's make a modifier that uses one value from the value space
to mean 'has no value'. It is not uncommon in some low level code to see
and int
being used, and the value -1
to mean no value. We can call it
optional<N>
, where N
is the 'has no value' value, and the interface mimics
that of std::optional
.
template <auto no_value>
struct optional
{
template <typename T>
struct modifier
{
// implementation here
};
};
This can already be used, but it's not very useful yet:
using my_type = strong::type<int, struct tag_, optional<0>>;
static_assert(strong::type_is_v<my_type, optional<0>);
Let's add some functionality to the mixin. Since the strong type inherits
publically from the modifier<>
template, any public member function declared
here becomes available from the strong type itself.
template <auto no_value>
struct optional
{
template <typename T> // This is the strong type
struct modifier
{
constexpr bool has_value() const noexcept
{
auto& self = static_cast<const T&>(*this);
return value_of(self) != no_value;
}
};
};
Since the modifier
mixin inherits from the strong type, it is always safe to
static_cast<>
to the
strong type.
It is now possible to query your strong type if it has a value or not.
using my_type = strong::type<int, struct tag_, optional<0>>;
static_assert(my_type{3}.has_value());
stacic_assert(! my_type{0}.has_value());
std::optional<>
also has operator*
to get the underlying value, without
checking if it's valid. Let's add that too.
template <auto no_value>
struct optional
{
template <typename T> // This is the strong type
struct modifier
{
constexpr bool has_value() const noexcept;
constexpr strong::underlying_type_t<T>& operator*() noexcept
{
auto& self = static_cast<T&>(*this);
return value_of(self);
}
constexpr const strong::underlying_type_t<T>& operator*() const noexcept
{
auto& self = static_cast<const T&>(*this);
return value_of(self);
}
};
};
If you want to move out of r-values, you need special overloads for that too,
which unfortunately makes the code quite repetitive. Writing the operators as
friend functions, taking the T
as a parameter removes the need for the casts.
template <auto no_value>
struct optional
{
template <typename T> // This is the strong type
struct modifier
{
constexpr bool has_value() const noexcept;
friend constexpr decltype(auto) operator*(T& self) noexcept
{
return value_of(self);
}
friend constexpr decltype(auto) operator*(const T& self) noexcept
{
return value_of(self);
}
friend constexpr decltype(auto) operator*(T&& self) noexcept
{
return value_of(std::move(self));
}
friend constexpr decltype(auto) operator*(const T&& self) noexcept
{
return value_of(std::move(self));
}
};
};
std::optional<>
also has member functions .value()
, which returns the value
if there is one, or throws.
template <auto no_value>
struct optional
{
template <typename T> // This is the strong type
struct modifier
{
constexpr bool has_value() const noexcept;
constexpr friend decltype(auto) operator*(T& t) noexcept;
constexpr friend decltype(auto) operator*(T&& t) noexcept;
constexpr friend decltype(auto) operator*(const T& t) noexcept;
constexpr friend decltype(auto) operator*(const T&& t) noexcept;
strong::underlying_type_t<T>& value()
{
if (!has_value() {
throw std::bad_optional_access();
}
auto& self = static_cast<cT&>(*this);
return value_of(self);
}
const strong::underlying_type_t<T>& value() const
{
if (!has_value() {
throw std::bad_optional_access();
}
auto& self = static_cast<cconst T&>(*this);
return value_of(self);
}
// ... and more
};
};
Unfortunately there is little that can be done to reduce the repetition. A bit can be done by writing a static helper function template:
template <auto no_value>
struct optional
{
template <typename T> // This is the strong type
struct modifier
{
constexpr bool has_value() const noexcept;
constexpr friend decltype(auto) operator*(T& t) noexcept;
constexpr friend decltype(auto) operator*(T&& t) noexcept;
constexpr friend decltype(auto) operator*(const T& t) noexcept;
constexpr friend decltype(auto) operator*(const T&& t) noexcept;
decltype(auto) value() &
{
return get_value(static_cast<T&>(*this));
}
decltype(auto) value() const &
{
return get_value(static_cast<const T&>(*this));
}
decltype(auto) value() &&
{
return get_value(static_cast<T&&>(*this));
}
decltype(auto) value() const &&
{
return get_value(static_cast<const T&&>(*this));
}
private:
template <typename TT>
static constexpr decltype(auto) get_value(TT&& self)
{
if (!self.has_value()) {
throw std::bad_optional_access();
}
return value_of(std::forward<TT>(self));
}
};
};
Here's the full implementation:
template <auto no_value>
struct optional
{
template <typename T>
struct modifier
{
constexpr bool has_value() const noexcept
{
auto& self = static_cast<const T&>(*this);
return value_of(self) != no_value;
}
friend constexpr decltype(auto) operator*(T&& self) noexcept
{
return value_of(std::move(self));
}
friend constexpr decltype(auto) operator*(const T&& self) noexcept
{
return value_of(std::move(self));
}
friend constexpr decltype(auto) operator*(T& self) noexcept
{
return value_of(self);
}
friend constexpr decltype(auto) operator*(const T& self) noexcept
{
return value_of(self);
}
constexpr decltype(auto) value() &
{
return get_value(static_cast<T&>(*this));
}
constexpr decltype(auto) value() const &
{
return get_value(static_cast<const T&>(*this));
}
constexpr decltype(auto) value() &&
{
return get_value(static_cast<T&&>(*this));
}
constexpr decltype(auto) value() const &&
{
return get_value(static_cast<const T&&>(*this));
}
private:
template <typename TT>
static constexpr decltype(auto) get_value(TT&& t)
{
if (!t.has_value()) {
throw std::bad_optional_access();
}
return value_of(std::forward<TT>(t));
}
};
};
To build the self-test program(s):
cmake <strong_type_dir> -DSTRONG_TYPE_UNIT_TEST=yes
cmake --build .
This will produce the test programs self_test
, and conditionally also
test_fmt8
,test_fmt9
, test_fmt10
and test_fmt11
, depending on which version(s) of
{fmt}
N.B. Microsoft Visual Studio MSVC compiler < 19.22 does not handle constexpr
correctly. Those found to cause trouble are disabled for those versions.
Library | Author |
---|---|
type_safe | Jonathan Müller |
NamedType | Jonathan Boccara |
strong_typedef | Anthony Williams (justsoftwaresolutions) |
Jonathan Boccara from MeetingC++ 2017 | |
Barney Dellar from C++OnSea 2019 | |
Björn Fahller from ACCU 2018 | |
Adi Shavit & Björn Fahller from NDC{Oslo} 2019 |
Discussions, pull-requests, flames are welcome.