/strong_type

An additive strong typedef library for C++14/17/20

Primary LanguageC++Boost Software License 1.0BSL-1.0

strong_type

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

CI Build Status codecov

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

  • 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 whith 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.

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

This repetition quictly gets old. You can use a template trick to get rid of it:

template <auto no_value>
struct optional
{
    template <typename T> // This is the strong type
    struct modifier
    {
        constexpr bool has_value() const noexcept;
        template <typename TT>
        constexpr friend decltype(auto) operator*(TT&& tt) noexcept
        {
            return value_of(std::forward<TT>(tt));
        }
    };
};

Yes, operators can be written as inline friends, which then accepts the type of the object as the parameter. This is often referred to as the "hidden friend idiom". By making the type a forwarding reference, this will match T&, T&&, const T&, const T&&, volatile T&, volatile T&&,const volatile T& and const volatile T&&. 8 overloads in one definition! Not bad.

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;
        template <typename TT>
        constexpr friend decltype(auto) operator*(TT&& tt) 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 here. 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;
        template <typename TT>
        constexpr friend decltype(auto) operator*(TT&& tt) 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;
        }
        template <typename TT>
        friend constexpr decltype(auto) operator*(TT&& self) noexcept
        {
            return value_of(std::forward<TT>(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 and test_fmt9, 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)
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.

@bjorn_fahller