Skip to content

utl::strong_type

<- to README.md

<- to implementation.hpp

utl::strong_type is a header providing templates for creating strong typedefs.

By default, typedefs in C and C++ are weak, which means separate typedefs don't count as distinct types:

using Offset = std::size_t;
using Size   = std::size_t;

static_assert(std::is_same_v<Offset, Size>); // types are the same

Strong typedefs can be used to mark types as distinct:

using Offset = strong_type::Arithmetic<std::size_t, class OffsetTag>;
using Size   = strong_type::Arithmetic<std::size_t, class   SizeTag>;

static_assert(!std::is_same_v<Offset, Size>); // types are different

This is useful for improving type safety. Arithmetic strong types act like thin wrappers around the underlying value and support all of the usual operations, but preserve type and disallow unwanted implicit conversions at no runtime cost.

Strong types are often used in physical modeling together with <chrono>-like ratio conversions to ensure dimensional correctness of the expressions. In a more general case they can protect against mixing up conceptually different values (such as IDs, offsets, sizes and etc.) which would otherwise be implicitly convertible to each other.

In addition, strong types are exceedingly useful for wrapping C APIs which tend to use regular integers and type-erased pointers for distinctly different values (whereas C++ would usually use classes and strongly typed enum class). This is particularly common for various system handles, which is why this header also provides strong_type::Unique<> that can wrap arbitrary handles into RAII semantics with a custom deleter (see OpenGL example).

Definitions

// Function binding
template <auto function>
struct Bind {
    template <class... Args> constexpr auto operator()(Args&&... args) const;
};

// Strongly typed move-only wrapper around 'T'
template <class T, class Tag, class Deleter = void>
class Unique {
    // Member types
    using   value_type = T;
    using     tag_type = Tag;
    using deleter_type = Deleter;

    // Move-only semantics
    constexpr Unique           (const Unique& ) =  delete;
    constexpr Unique& operator=(const Unique& ) =  delete;
    constexpr Unique           (      Unique&&);
    constexpr Unique& operator=(      Unique&&);

    // Conversion
    constexpr Unique           (T&& value) noexcept;
    constexpr Unique& operator=(T&& value) noexcept;

    // Accessing the underlying value
    constexpr const T& get() const noexcept;
    constexpr       T& get()       noexcept;
};

// Strongly typed arithmetic wrapper around 'T'
template <class T, class Tag>
struct Arithmetic {
    // Member types
    using value_type = T;
    using   tag_type = Tag;

    // Conversion
    constexpr Arithmetic           (T value) noexcept;
    constexpr Arithmetic& operator=(T value) noexcept;

    // Accessing the underlying value
    constexpr const T& get() const noexcept;
    constexpr       T& get()       noexcept;

    // Explicit cast
    template <class To> constexpr explicit operator To() const noexcept;

    // + all arithmetic operators supported by 'T'
    // + std::swap() support
};

Note

Strictly speaking, some of the noexcept modifiers listed here are inferred from std::is_nothrow_move_constructible_v<T> and other traits. In practice types that can throw during a move are extremely rare for our use case, so it usually holds true.

Methods

Function binding

template <auto function>
struct Bind {
    template <class... Args> constexpr auto operator()(Args&&... args) const;
};

Binds function to a stateless class so it can be passed as a template parameter.

Useful for passing functions pointers as custom deleters to std::unique_ptr<> and strong_type::Unique<>.

Note: Calling Bind<function>{}(args...) is equivalent to calling function(args...).

Unique

Member types

using   value_type = T;
using     tag_type = Tag;
using deleter_type = Deleter;

Member types reflecting the template parameters.

T can be an instance of any movable type. Default constructor is optional.

Tag is an arbitrary class used to discriminate this type from the others.

Deleter should either be void or a stateless class invocable for T&&.

Move-only semantics

constexpr Unique           (const Unique& ) =  delete;
constexpr Unique& operator=(const Unique& ) =  delete;
constexpr Unique           (      Unique&&);
constexpr Unique& operator=(      Unique&&);

Unique<> is a move-only type that behaves similarly to std::unique_ptr<>, but can hold an arbitrary internal value.

Conversion

constexpr Unique           (T&& value) noexcept;
constexpr Unique& operator=(T&& value) noexcept;

Constructor / assignment that takes an ownership of the value.

Note: Previous value (if present) will be destroyed according to the Deleter (if present).

Accessing the underlying value

constexpr const T& get() const noexcept;
constexpr       T& get()       noexcept;

Returns a constant or mutable reference to the managed object.

Important: Moving the underlying object out or deleting it can break class invariants. This is impossible to protect against similarly to how std::unique_ptr<> would cause a double-delete should the user call delete ptr.get() manually.

Arithmetic

Member types

using value_type = T;
using   tag_type = Tag;

Member types reflecting the template parameters.

T can be an instance of any arithmetic type (aka integer or float).

Tag is an arbitrary class used to discriminate this type from the others.

Conversion

constexpr Arithmetic           (T value) noexcept;
constexpr Arithmetic& operator=(T value) noexcept;

Constructor / assignment that assigns the underlying value.

Accessing the underlying value

constexpr const T& get() const noexcept;
constexpr       T& get()       noexcept;

Returns a constant or mutable reference to the underlying value.

Explicit cast

template <class To> constexpr explicit operator To() const noexcept;

Explicitly casting Arithmetic<T> is equivalent to performing the cast on its underlying value.

Implicit casts are intentionally prohibited.

Operators

Arithmetic<T> supports the same set of binary / unary operators as its underlying value_type.

The only exception to this rule is operator!() which is intentionally prohibited similarly to implicit casts.

std::swap() support is also provided.

Examples

Wrapping <cstdio> file handle

[ Run this code ] [ Open source file ]

using namespace utl;

// Create strongly typed wrapper around <cstdio> file handle
// (aka 'FILE*') with move-only semantics and RAII cleanup
using FileHandle = strong_type::Unique<std::FILE*, class FileTag, strong_type::Bind<&std::fclose>>;

FileHandle file = std::fopen("temp.txt", "w");

// upon destruction invokes 'fclose()' on the internal pointer,
// same principle works for most handles produced by 'C' APIs

Wrapping OpenGL shader handle

Note

OpenGL is a graphics API written in C. It uses unsigned int IDs as handles to the objects living in a GPU memory (buffers, shaders, pipelines and etc.). This is a perfect example of an API which greatly benefits from the stronger type safety and automatic cleanup of strong_type::Unique<>.

[ Run this code ] [ Open source file ]

// Mock of an OpenGL API
using GLuint = unsigned int;
using GLenum = unsigned int;

GLuint glCreateShader ([[maybe_unused]] GLenum shader_type) { return 0; }
void   glCompileShader([[maybe_unused]] GLuint shader_id  ) {           }
void   glDeleteShader ([[maybe_unused]] GLuint shader_id  ) {           }

#define GL_VERTEX_SHADER 1

// ...

using namespace utl;

// Create strongly typed wrapper around OpenGL shader handle 
// (aka 'unsigned int') with move-only semantics and RAII cleanup
using ShaderHandle = strong_type::Unique<GLuint, class ShaderTag, strong_type::Bind<&glDeleteShader>>;

ShaderHandle shader = glCreateShader(GL_VERTEX_SHADER);

// <real OpenGL would also have some boilerplate here>

// Retrieve the underlying value and pass it to a 'C' API
glCompileShader(shader.get());

Strongly typed integer unit

[ Run this code ] [ Open source file ]

using ByteOffset = utl::strong_type::Arithmetic<int, struct OffsetTag>;

constexpr ByteOffset buffer_start  = 0;
constexpr ByteOffset buffer_stride = 3;

// Perform arithmetics
static_assert(buffer_start + buffer_stride == ByteOffset{3});
static_assert(           2 * buffer_stride == ByteOffset{6});

// Extract value
static_assert(buffer_stride.get() == 3);

// Explicit cast
static_assert(static_cast<int>(buffer_stride) == 3);

// Compile time protection
constexpr int        element_count = 70;
constexpr ByteOffset buffer_end    = buffer_start + element_count * buffer_stride;

// > constexpr ByteOffset buffer_end = buffer_start + element_count;
//   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//   forgot to multiply by stride, will not compile

static_assert(buffer_end == ByteOffset{0 + 3 * 70});