Skip to content

rollbear/strong_type

Repository files navigation

strong_type

An additive strong typedef library for C++14/17/20 using the Boost Software License 1.0

CI Build Status codecov

Buy me a coffee

Intro

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_type uses the std library module, with import std; if you define the macro STRONG_TYPE_IMPORT_STD_LIBRARY=1.

Modifiers:

  • strong::affine_point<D> allows instances to be subtracted (yielding a D) or to add or subtract a D to an instance. See Affine Space. Examples of one dimensional affine points are pointer (with D being ptrdiff_t,) or std::time_point<> (with std::duration<> as D.) An example of a multidimensional affine point is a coordinate (with a vector type for D.) D can be defaulted, using strong::affine_point<>, in which case the difference type shares the same tag. The difference type from a strong::affine_point<D> can be obtained using type::difference, regardless of whether D is explicit or defaulted. It is natural that D is of a strong::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.

  • strong::arithmetic allows addition, subtraction, multiplication, division and remainder of instances.

    std::numeric_limits<T> is specialized for types using the strong::arithmetic modifier.

    Available in strong_type/arithmetic.hpp.

  • strong::bicrementable. Obviously a made up word for the occasion. Implements both strong::incrementable and strong::decrementable.

    Available in strong_type/bicrementable.hpp

  • strong::bitarithmetic allows bitwise &, bitwise |, bitwise ^ and shift operations.

    Available in strong_type/bitarithmetic.hpp.

  • strong::boolean provides explicit operator bool() const, providing the underlying type supports it.

    Available in strong_type/boolean.hpp.

  • strong::convertible_to<Ts...> provides an explicit operator Ts() const for each type Ts, providing the underlying type supports it.

    Available in strong_type/convertible_to.hpp.

  • strong::decrementable. Provides operator-- for the strong type, using the operator of the underlying type.

    Available in strong_type/incrementable.hpp

  • strong::default_constructible. The strong type is not default constructible by default. This modifier enables a default constructor which uses a default constructor of the underlying type.

    Available in strong_type/type.hpp

  • strong::difference allows instances to be subtracted and added (yielding a strong::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 another strong::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. A strong::difference is also strong::equality.

    Available in strong_type/difference.hpp.

  • strong::equality provides operators == and !=. The strong type can be compared for equality or inequality.

    Available in strong_type/equality.hpp.

  • strong::equality_with<Ts...> provides operators == and != between the strong type and each of the types Ts.... Note! While Ts can include other strong types, it can not refer to the strong type being defined. Use strong::equality for that.

    Available in strong_type/equality_with.hpp.

  • strong::formattable adds std::format and/or fmt::format capability, based on availability of the formatting library. This can further be controlled (globally) with the defines STRONG_HAS_STD_FORMAT respectively STRONG_HAS_FMT_FORMAT. With 0 to disable the support completely, and with 1 to force the support, disable the auto-detection.

    fmt::format allows formatting also types that are strong::ostreamable.

    Available in strong_type/formattable.hpp.

  • strong::hashable allows std::hash<> on the type (forwards to the underlying type,) to allow use in std::unordered_set<> and std::unordered_map<>.

    Available in strong_type/hashable.hpp.

  • strong::implicitly_convertible_to<Ts...> provides an operator Ts() const for each type Ts, providing the underlying type supports it.

    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

  • strong::indexed<D> allows use of the subscript operator[] on type D. This also allows member function at(D), providing the underlying type supports it. A lame version indexed<> allows subscript on any type that works.

    Available in strong_type/indexed.hpp.

  • strong::invocable provides operator() for the strong type, using the operator of the underlying type.

    Available in strong_type/invocable.hpp

  • strong::iostreamable. Both strong::istreamable and strong::ostreamable.

    Available in strong_type/iostreamable.hpp

  • strong::istreamable. Provides the default istream extraction operator>> for the strong type, as handled by the underlying type. Provide your own operator instead 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 a random_access_iterator, the strong type is strong::indexed<> and strong::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

  • strong::ordered provides operators <, <=, >= and >. The strong type offers the same ordering relation as the underlying type.

    Available in strong_type/ordered.hpp

  • strong::ordered_with<Ts...> provides operators <, <=, >= and > between the strong type and each of the types Ts.... Note! While Ts can include other strong types, it cannot refer to the strong type being defined. Use strong::ordered for that.

    Available in strong_type/ordered_with.hpp

  • strong::ostreamable. Provides the default ostream insertion operator<< 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 relation as the underlying type. The result is std::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 types Ts.... Note! While Ts can include other strong types, it cannot refer to the strong type being defined. Use strong::partially_ordered for that. The result is std::partial_ordering. Note! This does not imply ´strong::equality_with<Ts...>´.

    Available in strong_type/ordered_with.hpp

  • strong::pointer allows operator* and operator->, and comparisons with nullptr providing the underlying type supports it.

    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 types iterator and const_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 as strong::semiregular and strong::equality. A good default base for most types.

    Available in strong_type/regular.hpp

  • strong::scalable_with<Ts...> Allows multiplying and dividing the value with each type Ts, providing the underlying type supports it. It also allows dividing instances of scalable_with<>, if the underlying type supports it, and returns the first type in the list of Ts....

    Available in strong_type/scalable_with.hpp

  • strong::semiregular. This gives you strong::default_constructible move/copy constructible, move/copy assignable and swappable. A decent default for many types.

    Available in strong_type/semiregular.hpp.

  • strong::strongly_ordered provides operator <=> The strong type offers the same ordering relation as the underlying type. The result is std::strong_ordering. Note! This does not imply [strong::equality<Ts...>(#equality).

    Available in strong_type/ordered.hpp

  • strong::strongly_ordered_with<Ts...> provides operator <=> between the strong type and each of the types Ts.... Note! While Ts can include other strong types, it cannot refer to the strong type being defined. Use strong::strongly_ordered for that. The result is std::strong_ordering Note! This does not imply strong::equality_with<Ts...>.

    Available in strong_type/ordered_with.hpp

  • strong::unique. Make the type move constructible and move assignable but not copy constructible nor copy assignable.

    Available in strong_type/unique.hpp

  • strong::weakly_ordered provides operator <=> The strong type offers the same ordering relation as the underlying type. The result is std::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 types Ts.... Note! While Ts can include other strong types, it cannot refer to the strong type being defined. Use strong::weakly_ordered for that. The result is std::weak_ordering Note! This does not imply strong::equality_with<Ts...>.

    Available in strong_type/ordered_with.hpp

Utilities:

A number of small utilities are available directly in strong_type/type.hpp.

  • strong::type provides a non-member swap() function as a friend, which swaps underlying values using std::swap or a specific swap function for the underlying type.

  • strong::underlying_type<Type> is T for strong::type<T, Tag, Ms...> and public descendants, and Type for other types.

  • strong::uninitialized can be used to construct instances of strong::type<T...> without initializing the value. This is only possible if the underlying type is trivially 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 with the value of strong::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, like strong::ordered_with<Ts...>, it tests each of the types Ts 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_asserts above pass.

Tips!

Using aliases to define similar types

If you want several types that are, in essence, the same, but still distinguished as unique types, you can use an alias that takes a tag type. E.g.

template <typename Tag>
using counter = strong::type<uint64_t, Tag, strong::regular, strong::ordered, strong::incrementable, strong::equality_with<uint64_t>>;

using request_counter = counter<struct request_tag>;
using error_counter = counter<struct error_tag>;
using timeout_counter = counter<struct timeout_tag>;

Writing a modifier

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 publicly 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 publicly 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));
        }
    };
};

Self test

To build the self-test program(s):

cmake <strong_type_dir> -DSTRONG_TYPE_UNIT_TEST=yes
cmake --build .

You can also add the option -DSTRONG_TYPE_IMPORT_STD_LIBRARY=yes if your development environment supports C++20 "import std;"

The build 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.

Other libraries:

Library Author
type_safe Jonathan Müller
NamedType Jonathan Boccara
strong_typedef Anthony Williams (justsoftwaresolutions)

Presentations about defining and using strong types

Strong Types for Strong Interfaces Jonathan Boccara from MeetingC++ 2017
Strong Types in C++ Barney Dellar from C++OnSea 2019
Type Safe C++? - LOL! - ;-) Björn Fahller from ACCU 2018
Curiously Coupled Types Adi Shavit & Björn Fahller from NDC{Oslo} 2019

Discussions, pull-requests, flames are welcome.

https://fosstodon.org/@rollbear, @rollbear.bsky.social