Introduction

stdx has several aims:

  • Where possible, to enable using parts of the C++ standard library in versions earlier than when they were introduced, and where standard library implementations are lagging behind the standard.

  • To provide a common interface across C++ versions such that preprocessor directives in application code are minimized.

  • To provide implementations of standard behaviour that are richer or more efficient — at either compile-time or runtime — than the standard versions.

  • To provide useful functionality that is missing from the C++ standard but arguably belongs at a base level.

Compiler and C++ version support

The following compilers are supported:

  • clang 14

  • clang 15

  • clang 16

  • clang 17

  • clang 18

  • gcc 12

  • gcc 13

In general, stdx supports the C++17 standard; however some functionality is available only in 20 and later.

atomic.hpp

atomic.hpp provides an implementation of std::atomic with a few differences.

stdx::atomic does not implement:

  • is_lock_free or is_always_lock_free

  • compare_exchange_{weak,strong}

  • wait

  • notify_{one,all}

  • fetch_{max,min}

However, stdx::atomic allows customization of the atomic implementation for best codegen. stdx::atomic is implemented using the atomic API exposed by Intel’s baremetal concurrency library.

For example, it is possible that a particular platform requires atomic accesses to be 32-bit aligned. To achieve that for stdx::atomic<bool>, we could provide a configuration header specializing ::atomic::alignment_of:

// this header: atomic_cfg.hpp
#include <cstdint>

template <>
constexpr inline auto ::atomic::alignment_of<bool> = alignof(std::uint32_t);

To apply this configuration, when compiling, pass -DATOMIC_CFG="<path>/atomic_cfg.hpp". The result would be that stdx::atomic<bool> has 32-bit alignment:

static_assert(alignof(stdx::atomic<bool>) == alignof(std::uint32_t));

Using the baremetal concurrency library it is possible to override the handling of atomic access (load, store, exchange, fetch_<op>) to ensure the best codegen on a particular platform. As well as alignment concerns, for instance it may be the case on a single-core microcontroller that it is cheaper to disable and re-enable interrupts around a read/write than incurring a lock-free atomic access.

atomic_bitset.hpp

atomic_bitset.hpp provides an implementation of a bitset with atomic semantics.

An atomic_bitset is limited in size to the maximum integral type a platform can support while still using lock-free atomic instructions. Like bitset, it can be defined by selecting the underlying storage type automatically:

using A = stdx::bitset<8>;   // uses uint8_t
using B = stdx::bitset<16>;  // uses uint16_t
using C = stdx::bitset<32>;  // uses uint32_t
using D = stdx::bitset<64>;  // uses uint64_t

atomic_bitset is constructed in the same way as bitset: with all_bits, place_bits, a value, or a string_view:

using namespace std::string_view_literals;
auto bs0 = stdx::atomic_bitset<8>{};
auto bs1 = stdx::atomic_bitset<8>{stdx::all_bits};            // 0b1111'1111
auto bs2 = stdx::atomic_bitset<8>{stdx::place_bits, 0, 1, 3}; // 0b1011
auto bs3 = stdx::atomic_bitset<8>{0b1011};
auto bs4 = stdx::atomic_bitset<8>{"1011"sv};
Note
atomic_bitset​'s constructors are constexpr, but none of the other functions are.

Also like bitset, atomic_bitset supports conversion to integral types:

auto bs = stdx::atomic_bitset<11>{0b101}; // 11 bits, value 5
auto i = bs.to<std::uint64_t>();          // 5 (a std::uint64_t)
auto j = bs.to_natural();                 // 5 (a std::uint16_t)

And operation with enumeration types:

enum struct Bits { ZERO, ONE, TWO, THREE, MAX };
auto bs = stdx::atomic_bitset<Bits::MAX>{stdx::all_bits}; // 4 bits, value 0b1111
bs.set(Bits::ZERO);
bs.reset(Bits::ZERO);
bs.flip(Bits::ZERO);
auto bit_zero = bs[Bits::ZERO];

Unlike bitset, atomic_bitset​'s operations are atomic. For example, load and store are basic operations that return and take a corresponding bitset:

constexpr auto bs = stdx::atomic_bitset<8>{0b1010ul};
auto copy = bs.load(); // a stdx::bitset<8>{0b1010ul};
bs.store(copy);

Like load and store on std::atomic, the load and store operations on stdx::atomic_bitset take an optional std::memory_order. stdx::atomic_bitset is also implicitly convertible to a corresponding stdx::bitset; that operation is equivalent to load().

The set, reset and flip operations also take an optional std::memory_order: these operations are equivalent to store in their semantics, except that they return the stdx::bitset that was the previous value.

constexpr auto bs = stdx::atomic_bitset<8>{0b1010ul};
auto prev = bs.set(0);
// bs   == 1011
// prev == 1010 (stdx::bitset<8>)
Note
When set or reset are called without specifying bits, they return a reference to the atomic_bitset. This is because these operations result in a plain store which does not return the previous value.

all, any, none and count are also available on atomic_bitset and they are each equivalent to load followed by the respective operation. Like load, they also take an optional std::memory_order.

So what is not available on atomic_bitset?

  • any binary operation: equality, binary versions of and, or, etc.

  • bit shift operations

  • for_each and lowest_unset

  • unary not

These operations are not provided for varying reasons:

  • atomic semantics are impossible or problematic to guarantee (binary operations)

  • atomic instructions are not available (bit shifts, lowest_unset)

  • atomic semantics are unclear (for_each)

  • the caller can easily achieve what they want (unary not)

In all of these cases though, the caller can make the right choice for them, and use the corresponding operations on bitset after correctly reasoning about the required semantics.

algorithm.hpp

algorithm.hpp provides an implementation of some algorithms.

for_each and for_each_n

stdx::for_each is similar to std::for_each, but variadic in its inputs.

template <typename InputIt, typename Operation, typename... InputItN>
constexpr auto for_each(InputIt first, InputIt last,
                        Operation op, InputItN... first_n) -> Operation;
Note
stdx::for_each is constexpr in C++20 and later, because it uses std::invoke.

stdx::for_each_n is just like stdx::for_each, but instead of taking two iterators to delimit the input range, it takes an iterator and size.

template <typename InputIt, typename Size, typename Operation,
          typename... InputItN>
constexpr auto for_each_n(InputIt first, Size n,
                          Operation op, InputItN... first_n) -> Operation;

transform and transform_n

stdx::transform is similar to std::transform, but variadic in its inputs.

template <typename InputIt, typename OutputIt, typename Operation, typename... InputItN>
constexpr auto transform(InputIt first, InputIt last, OutputIt d_first,
                         Operation op, InputItN... first_n)
    -> transform_result<OutputIt, InputIt, InputItN...>;

Without the variadic pack InputItN…​ here, a call site looks like regular unary std::transform. But with the addition of the pack, stdx::transform becomes more like std::ranges::views::zip_transform. It can transform using an n-ary function and n sequences.

Note
The return value is equivalent to a tuple<OutputIt, InputIt, InputItN…​>. In C++20 and later this is a stdx::tuple, in C++17 a std::tuple.
Note
stdx::transform is constexpr in C++20 and later, because it uses std::invoke.

stdx::transform_n is just like stdx::transform, but instead of taking two iterators to delimit the input range, it takes an iterator and size.

template <typename InputIt, typename Size, typename OutputIt, typename Operation,
          typename... InputItN>
constexpr auto transform(InputIt first, Size n, OutputIt d_first,
                         Operation op, InputItN... first_n)
    -> transform_result<OutputIt, InputIt, InputItN...>;

bit.hpp

bit.hpp provides an implementation that mirrors <bit>, but is constexpr in C++17. It is mostly based on intrinsics.

bit_mask

bit_mask is a function for constructing a bit mask between most-significant and least-significant bits.

constexpr auto x = stdx::bit_mask<std::uint8_t, 5, 3>();
static_assert(x == std::uint8_t{0b0011'1000});

// Lsb may be omitted (meaning it's 0)
constexpr auto y = stdx::bit_mask<std::uint8_t, 5>();
static_assert(y == std::uint8_t{0b0011'1111});

// omitting both Msb and Lsb means the entire range of the type
constexpr auto z = stdx::bit_mask<std::uint8_t>();
static_assert(z == std::uint8_t{0b1111'1111});

Msb and Lsb denote a closed (inclusive) range where Msb >= Lsb. The first template argument must be an unsigned integral type.

bit_mask is also available for use with "normal" value arguments rather than template arguments:

constexpr auto x = stdx::bit_mask<std::uint8_t>(5, 3);
static_assert(x == std::uint8_t{0b0011'1000});

// Lsb may be omitted (meaning it's 0)
constexpr auto y = stdx::bit_mask<std::uint8_t>(5);
static_assert(y == std::uint8_t{0b0011'1111});

bit_pack

bit_pack is a function for packing multiple unsigned integral values into a larger bit width value.

constexpr auto x = stdx::bit_pack<std::uint32_t>(0x12, 0x34, 0x56, 0x78);
static_assert(x == 0x1234'5678);
constexpr auto y = stdx::bit_pack<std::uint32_t>(0x1234, 0x5678);
static_assert(y == x);

bit_pack can be used:

  • to pack 2 std::uint8_t​s into a std::uint16_t

  • to pack 2 std::uint16_t​s into a std::uint32_t

  • to pack 2 std::uint32_t​s into a std::uint64_t

  • to pack 4 std::uint8_t​s into a std::uint32_t

  • to pack 4 std::uint16_t​s into a std::uint64_t

  • to pack 8 std::uint8_t​s into a std::uint64_t

The arguments are listed in order of significance, i.e. for the binary overloads, the first argument is the high bits, and the second argument the low bits.

bit_size

bit_size returns a std::size_t: the size of its type argument in bits. For unsigned integral types, this is equivalent to std::numeric_limits<T>::digits.

constexpr std::size_t x = stdx::bit_size<std::uint8_t>();
static_assert(x == 8);

smallest_uint

smallest_uint is a function template that selects the smallest unsigned integral type that will fit a compile-time value.

constexpr auto x = stdx::smallest_uint<42>();   // std::uint8_t{}
constexpr auto y = stdx::smallest_uint<1337>(); // std::uint16_t{}

// smallest_uint_t is the type of a call to smallest_uint
using T = stdx::smallest_uint_t<1337>; // std::uint16_t

to_le and to_be

to_le and to_be are variations on byteswap that convert unsigned integral types to little- or big-endian respectively. On a little-endian machine, to_le does nothing, and to_be is the equivalent of byteswap. On a big endian machine it is the other way around.

constexpr auto x = std::uint32_t{0x12'34'56'78};
constexpr auto y = stdx::to_be(x); // 0x78'56'34'12 (on a little-endian machine)

to_le and to_be are defined for unsigned integral types. Of course for std::uint8_t they do nothing.

bitset.hpp

bitset.hpp provides an implementation that mirrors std::bitset, but is constexpr in C++17 and has the following differences:

The underlying type can be specified: stdx::bitset<8, std::uint16_t> defines a bitset with 8 bits whose storage type is std::uint16_t. The storage type must be an unsigned integral type. It controls the value_type of the underlying storage and hence may affect efficiency for some operations according to platform.

  • Stream input and output operators are not implemented.

  • A std::hash specialization is not implemented.

  • to_string, to_ulong and to_ullong are not implemented — but to and to_natural provide ways to convert to integral types.

  • operator[] is read-only: it does not return a proxy reference type

A bitset has two template parameters: the size of the bitset and the storage element type to use. The storage element type must be unsigned.

template <std::size_t N, typename Element = void>
struct bitset;

If the storage element type is omitted, the smallest unsigned type that will fit the size is selected, or std::uint64_t if the size is more than 64.

using A = stdx::bitset<8>;   // uses uint8_t
using B = stdx::bitset<16>;  // uses uint16_t
using C = stdx::bitset<32>;  // uses uint32_t
using D = stdx::bitset<64>;  // uses uint64_t
using E = stdx::bitset<128>; // uses (array of) uint64_t
using F = stdx::bitset<128, std::uint8_t>; // uses (array of) uint8_t

A bitset can be created from a std::uint64_t:

auto bs = stdx::bitset<8>{0b1100}; // bits 2 and 3 set

Or by specifying which bits are set and using stdx::place_bits:

auto bs = stdx::bitset<8>{stdx::place_bits, 2, 3}; // bits 2 and 3 set

Or with a string_view (potentially by substring and with a known value for set bits):

using namespace std::string_view_literals;
auto bs1 = stdx::bitset<8>{"1100"sv};            // bits 2 and 3 set
auto bs2 = stdx::bitset<8>{"1100"sv, 0, 2};      // bits 0 and 1 set
auto bs3 = stdx::bitset<8>{"AABB"sv, 0, 2, 'A'}; // bits 0 and 1 set

To convert a bitset back to an integral type, to<T> is available where T is an unsigned integral type large enough to fit all the bits. And to_natural produces the smallest such unsigned integral type.

auto bs = stdx::bitset<11>{0b101}; // 11 bits, value 5
auto i = bs.to<std::uint64_t>();   // 5 (a std::uint64_t)
auto j = bs.to_natural();          // 5 (a std::uint16_t)

Bitsets support all the usual bitwise operators (and, or, xor and not, shifts) and also support operator- meaning set difference, or a & ~b.

A bitset can also be used with an enumeration that represents bits:

enum struct Bits { ZERO, ONE, TWO, THREE, MAX };
auto bs = stdx::bitset<Bits::MAX>{stdx::all_bits}; // 4 bits, value 0b1111
bs.set(Bits::ZERO);
bs.reset(Bits::ZERO);
bs.flip(Bits::ZERO);
auto bit_zero = bs[Bits::ZERO];
Note
The enumeration values are the bit positions, not the bits themselves (the enumeration values are not fixed to powers-of-2).

A bitset also supports efficient iteration with for_each, which calls a function with each set bit in turn, working from LSB to MSB:

auto bs = stdx::bitset<8>{0b1010'1010ul};
for_each([&](auto i) { /* i == 1, 3, 5, 7 */ }, bs);

To support "external" iteration, or use cases like using a bitset to track used objects, lowest_unset is also provided:

auto bs = stdx::bitset<8>{0b11'0111ul};
auto i = bs.lowest_unset(); // i == 3

byterator.hpp

byterator.hpp provides a random-access-iterator-like type that offers some extra functionality for reading and writing values.

It is relatively common to have an array of bytes or words that represents a table that needs to be interpreted in some way. Values in that table could be interpreted as 8-bit values, or 16-bit values, or 32-bit values split across multiple bytes in the table. Values at particular points in the table may represent offsets, number of elements to process, etc.

In general, pointer-manipulating code like this is a bad idea:

std::uint8_t const table[1024];

std::uint8_t value1 = *table;
std::uint16_t value2 = *reinterpret_cast<std::uint16_t const *>(&table[1]);
// etc...

It’s a one-way ticket to undefined behaviour. Even if the hardware allows unaligned accesses.

byterator is designed to help deal with this situation, by providing a safe interface for reading and writing values like this. Create a byterator from an iterator:

std::uint8_t const table[1024];
auto i = stdx::byterator{std::begin(table)};
Note
A byterator must be constructed from a random access iterator whose value_type is trivially copyable.

A byterator is a random access iterator whose value_type is std::byte, and it can be used just like that. It supports all the requirements.

std::uint8_t const table[1024];
for (auto i = stdx::byterator{std::begin(table)}; i != std::end(table); ++i) {
  std::byte b = *i;
  // do something ...
}

But the real use case is when accessing heterogeneous types in the table:

std::uint8_t const table[1024];
auto i = stdx::byterator{std::begin(table)};
// read a std::uint16_t (and advance i by 2 bytes)
auto value = i.readu16();

byterator has the following convenience functions:

auto v8 = i.peeku8();   // read value without advancing
v8 = i.readu8();        // read and advance
i.writeu8(v8);          // write and advance

auto v16 = i.peeku16(); // read value without advancing
v16 = i.readu16();      // read and advance
i.writeu16(v16);        // write and advance

auto v32 = i.peeku32(); // read value without advancing
v32 = i.readu32();      // read and advance
i.writeu32(v32);        // write and advance

These convenience functions are implemented by function templates that offer more control, if needed:

// peek and read have identical arguments; read is shown

// read and return a std::uint16_t, advance 2 bytes
auto v1 = i.read<std::uint16_t>();

// read a std::uint8_t, advance 1 byte, but return the value as a std::uint16_t
auto v2 = i.read<std::uint8_t, std::uint16_t>();

The last case above is particularly useful for dealing with enumerations:

// E is a std::uint32_t under the hood, but we might well have a 1-byte value in the table
enum struct E : std::uint32_t { A, B, C };
auto v3 = i.read<std::uint8_t, E>();

cached.hpp

A cached value represents a value that is computed on demand once, and cached thereafter. It is constructed with a lambda expression that will compute the value when needed.

constexpr auto c = stdx::cached{[] { return expensive_computation(); }};

A cached value is something like a std::optional and supports some similar functionality. Note though that any kind of "dereference" operation automatically computes the value if needed.

// check whether the value is present
auto b = c.has_value();

// or, automatic bool conversion (explicit)
if (c) {
  // do something
}

// use the value (computing where necessary)
auto value = *c;
auto alt_value = c.value();
auto value_member = c->member;

// reset the value
c.reset();

reset means that the next time the value is needed, it will be recomputed. However, refresh immediately recomputes the value.

// immediate recomputation
c.refresh();

If needed, the type of the cached value can obtained with cached_value_t.

auto c = stdx::cached{[] { return expensive_computation(); }};
using V = stdx::cached_value_t<decltype(c)>;
Note
You can also use typename decltype(c)::value_type, but if the type of c has cvref qualifiers, cached_value_t saves the bother of using remove_cvref_t.

compiler.hpp

compiler.hpp provides macros for decorating declarations, which resolve either to keywords or to compiler-specific attributes:

concepts.hpp

concepts.hpp implements various standard concepts. In C++17 they are surfaced as compile-time boolean values.

Note
For compatibility with the standard and with std::is_base_of, a class is considered to be derived_from itself.
Note
range is in the stdx namespace, and is defined in terms of std::begin and std::end, unlike the standard which has std::ranges::range defined in terms of std::ranges::begin and std::ranges::end.

concepts.hpp also has a couple of non-standard but useful concepts.

callable

callable is modelled by functions by and objects with operator(). In particular it is true for generic lambda expressions, where operator() is a function template.

auto f() -> void {}
static_assert(stdx::callable<decltype(f)>);

auto lambda = [] (int i) { return i + 1; };
static_assert(stdx::callable<decltype(lambda)>);

auto generic_lambda = [] (auto i) { return i + 1; };
static_assert(stdx::callable<decltype(generic_lambda)>);

has_trait

has_trait is used to turn a type trait (standard or otherwise) into a concept. There are many type traits and comparatively few standard concepts; this concept helps bridge the gap more easily and concisely than writing out boilerplate concepts for type traits.

template <stdx::has_trait<std::is_class> T>
auto f(T) -> void {
  // can only be called with class types
}

For the purposes of has_trait, a type trait is a class template with one parameter that has a constexpr static bool value member.

same_as_unqualified

same_as_unqualified is true when two types are the same are removing top-level cv-qualifications and references, if any. It’s useful for constraining hidden friends — particularly when member functions would need to be replicated with different reference qualifiers before C++23.

struct S {
  // before C++20
  auto f() & { /* ... */ }
  auto f() const & { /* ... */ }
  auto f() && { /* ... */ }
  auto f() const && { /* ... */ }
  // (and 4 more for volatile qualifiers!)

  // with C++23's explicit object parameter ("deducing this")
  template <typename Self>
  auto f(this Self&& s) {
    // Self is perfectly-forwarded
  }

private:
  // hidden friend alternative
  template <same_as_unqualified<S> Self>
  friend auto f(Self&& s) {
    // Self is perfectly-forwarded
  }
};

ct_conversions.hpp

ct_conversions.hpp provides two compile-time functions for obtaining strings of type names and enumerator names.

template <typename T>
consteval auto type_as_string() -> std::string_view;

template <auto E>
consteval auto enum_as_string() -> std::string_view;

ct_format.hpp

ct_format.hpp provides ct_format, a compile-time function for formatting strings.

Note
Like ct_string, ct_format is available only in C++20 and later.

The format string is provided as a template argument, and the arguments to be formatted as regular function arguments.

The result type is a stdx::format_result containing two members: the ct_string and a tuple of the format arguments.

auto s = stdx::ct_format<"Hello {} {}">(42, 17);
// s is stdx::format_result{"Hello {} {}"_cts, stdx::tuple{42, 17}}

When format arguments are available at compile-time, wrapping them in CX_VALUE(…​) means they will get compile-time formatted.

auto s = stdx::ct_format<"Hello {} {}">(CX_VALUE(42), 17);
// s is stdx::format_result{"Hello 42 {}"_cts, stdx::tuple{17}}

If there are no runtime arguments, the result is just a stdx::ct_string:

auto s = stdx::ct_format<"Hello {} {}">(CX_VALUE(42), CX_VALUE(17));
// s is "Hello 42 17"_cts

Types and compile-time enumeration values are stringified thus:

enum struct E { value };
auto s = stdx::ct_format<"Hello {} {}">(CX_VALUE(int), CX_VALUE(E::value));
// s is "Hello int value"_cts
Note
Compile-time formatting is done with fmtlib and supports the same formatting DSL. Positional arguments are not supported.

ct_format is designed for use with a logging backend like the one in https://github.com/intel/compile-time-init-build. Hence stdx::format_result allows lazy runtime formatting. For the same reason, compile-time formatting can output string information in a suitable type rather than as a value. This is done by passing the template as a second template argument to ct_format.

auto s = stdx::ct_format<"{}", sc::string_constant>(CX_VALUE(42));
// s is sc::string_constant<char, '4', '2'>{}

When formatting a compile-time stdx::format_result, the strings and argument tuples are collapsed to a single stdx::format_result:

constexpr static auto a = stdx::ct_format<"The answer is {}">(42);
// a is stdx::format_result{"The answer is {}", stdx::tuple{42}}

constexpr static auto q = stdx::ct_format<"{}. But what is the question?">(CX_VALUE(a));
// q is stdx::format_result{"The answer is {}. But what is the question?", stdx::tuple{42}}

ct_string.hpp

A ct_string is a compile-time string that can be used as a non-type template parameter (NTTP).

Note
ct_string is available only in C++20 and later. It requires compiler support for using structural types as NTTPs.

Example usage:

template <ct_string S>
struct named_thing { ... };

auto my_thing = named_thing<"mine">{};

Here we declare a struct with an NTTP, and instantiate the template with a string. When compiled, "mine" will create a ct_string which is the NTTP passed to named_thing.

Note
ct_string is a class template. The declaration of named_thing here uses ct_string as a placeholder type for an NTTP, whose concrete type will be deduced. This is new for C++20 - see https://en.cppreference.com/w/cpp/language/template_parameters for details.

The ct_string interface:

template <ct_string S>
struct named_thing {
  template <ct_string Other>
  auto f() {
    // here we can:
    constexpr std::size_t sz = S.size();  // ask for ct_string's size
    constexpr bool is_empty = S.empty();  // ask whether a ct_string is empty
    constexpr bool equal = S == Other;    // compare two ct_strings

    // we can also convert to/from cib string constants
    constexpr auto cib_sc_string = stdx::ct_string_to_type<S, sc::string_constant>();
    constexpr auto stdx_ct_string = stdx::ct_string_from_type(cib_sc_string);
    static_assert(S == stdx_ct_string);

    // we can make a ct_string with a UDL:
    using namespace stdx::ct_string_literals;
    constexpr auto s = "Hello"_cts;

    // we can concatenate two ct_strings:
    constexpr auto s1 = "abc"_cts;
    constexpr auto s2 = "def"_cts;
    constexpr auto s3 = s1 + s2; // abcdef

    // and we can split a ct_string at the first occurrence of a character,
    // optaining a pair of ct_strings
    constexpr auto p = stdx::split<S, '/'>();
    // if the character doesn't exist, p.first is equal to S and p.second is empty
    // otherwise p.first is everything up to (but not including) the character,
    // and p.second is everything after (also not including)

    // we can also iterate over a ct_string
    for (auto c : S) { ... }

    // a ct_string can also explicitly convert to a std::string_view
    constexpr auto sv = static_cast<std::string_view>(S);

    // when required, we can access the underlying array of chars,
    // which includes the null terminator
    const auto& char_array = S.value;
  }
};
Note
size and empty are always available as constexpr.
Note
_cts is a user-defined literal declared in stdx::literals::ct_string_literals, where both literals and ct_string_literals are inline namespaces.
Caution
ct_string stores an internal array (value) which includes the null terminator for the string. The size reported by the ct_string is one less than the size of the internal array. Thus the begin, end and size of a ct_string all represent the string of characters without the null terminator. However, for interfacing with legacy functions, a null terminator can be useful.

See cib documentation for details about the cib string constant class.

cx_map.hpp

cx_map implements a constexpr-capable map with a compile-time capacity. The map is unordered.

template <typename Key,
          typename Value,
          std::size_t N>
class cx_map;

The cx_map interface:

template <typename K, typename V, std::size_t N>
auto f(stdx::cx_map<K, V, N> m) {
    // here we can:
    std::size_t sz = m.size(); // ask for m's size
    constexpr std::size_t cap = m.capacity(); // ask for m's capacity (same as N)
    bool is_empty = m.empty(); // ask whether a cx_map is empty
    bool is_full = m.full(); // ask whether a cx_map is full
    m.clear() // clear a cx_map

    // we can use some of the usual functions
    m.insert_or_assign(K{}, V{});
    m.put(K{}, V{}); // same as insert_or_assign
    V& v = m.get(K{});
    bool c = m.contains(K{});
    m.erase(K{});

    // and use iterators:
    // begin and end
    // cbegin and cend
    // (therefore also range-for loops)

    // we can also pop an arbitrary element
    auto [k, v] = m.pop_back();
};

insert_or_assign returns true if a value was inserted, false if it was assigned.

Note
capacity is always available as constexpr, even though m above is a function parameter and therefore not constexpr.

cx_multimap.hpp

cx_multimap implements a constexpr-capable multimap with a compile-time capacity. The multimap is unordered. As well as a key-capacity, each key has a limited number of values that can be associated with it (the value-capacity).

template <typename Key,
          typename Value,
          std::size_t KeyN,
          std::size_t ValueN = KeyN>
class cx_multimap;

The cx_multimap interface:

template <typename K, typename V, std::size_t KN, std::size_t VN>
auto f(stdx::cx_map<K, V, KN, VN> m) {
    // here we can:
    std::size_t sz = m.size(); // ask for m's size
    constexpr std::size_t cap = m.capacity(); // ask for m's capacity (same as KN)
    bool is_empty = m.empty(); // ask whether a cx_multimap is empty
    m.clear() // clear a cx_multimap

    m.insert(K{}); // make sure a key exists
    m.insert(K{}, V{}); // associate a value with a key
    m.put(K{}); // same as insert
    m.put(K{}, V{}); // same as insert

    auto& v = m.get(K{}); // v is a cx_set<V, VN>
    bool c1 = m.contains(K{});
    bool c2 = m.contains(K{}, V{});
    m.erase(K{});
    m.erase(K{}, V{});

    // and use iterators:
    // begin and end
    // cbegin and cend
    // (therefore also range-for loops)
};
Note
capacity is always available as constexpr, even though m above is a function parameter and therefore not constexpr.

cx_queue.hpp

cx_queue is a circular queue with a compile-time capacity and a policy that controls whether and how over/underflow is handled.

template <typename T, std::size_t N,
          typename OverflowPolicy = safe_overflow_policy>
class cx_queue;
Note
By default, cx_queue uses the safe_overflow_policy which means that over/underflow will result in a call to panic. unsafe_overflow_policy (which does no checking) is also available.

The cx_queue interface:

template <typename T, std::size_t N, typename P>
auto f(stdx::cx_queue<T, N, P> q) {
    // here we can:
    std::size_t sz = q.size(); // ask for q's size
    constexpr std::size_t cap = q.capacity(); // ask for q's capacity (same as N)
    bool is_empty = q.empty(); // ask whether a cx_queue is empty
    bool is_full = q.full(); // ask whether a cx_queue is full
    q.clear(); // clear a cx_queue

    // we can use some of the usual queue functions
    q.push(T{});
    T& t1 = q.front();
    T& t2 = q.back();
    T t = q.pop();
  }
};
Note
capacity is always available as constexpr, even though q above is a function parameter and therefore not constexpr.

Users of cx_queue may provide custom overflow policies. A policy must implement two (static) functions:

struct custom_overflow_policy {
    constexpr static auto check_push(std::size_t size, std::size_t capacity) -> void {
      // push is safe if size < capacity
      // otherwise, overflow panic
    }
    constexpr static auto check_pop(std::size_t size) -> void {
      // pop is safe if size > 0
      // otherwise, underflow panic
    }
};

cx_set.hpp

cx_set implements a constexpr-capable set with a compile-time capacity. The set is unordered.

template <typename Key, std::size_t N>
class cx_set;

The cx_set interface:

template <typename K, std::size_t N>
auto f(stdx::cx_set<K, N> s) {
    // here we can:
    std::size_t sz = s.size(); // ask for s's size
    constexpr std::size_t cap = s.capacity(); // ask for s's capacity (same as N)
    bool is_empty = m.empty(); // ask whether a cx_set is empty
    m.clear() // clear a cx_set

    // we can use some of the usual functions
    s.insert(K{});
    bool c = s.contains(K{});
    s.erase(K{});

    // and use iterators:
    // begin and end
    // cbegin and cend
    // (therefore also range-for loops)

    // we can also pop an arbitrary element
    auto k = s.pop_back();

    // and merge two sets (assuming s has capacity)
    s.merge(cx_set{1, 2, 3});
};

insert returns true if a value was inserted, false if it already existed.

Caution
When merging one set into another, the destination set must have enough capacity!
Note
capacity is always available as constexpr, even though s above is a function parameter and therefore not constexpr.

A cx_set may also be initialized with CTAD:

// s is a cx_set<int, 3>
auto s = cx_set{1, 2, 3};

cx_vector.hpp

cx_vector is a contiguous data structure with a compile-time capacity and variable (but bounded) size. Basically parts of the std::vector interface applied to a std::array.

template <typename T,
          std::size_t N>
class cx_vector;

The cx_vector interface:

template <typename T, std::size_t N>
auto f(stdx::cx_vector<T, N> v) {
    // here we can:
    std::size_t sz = v.size(); // ask for v's size
    constexpr std::size_t cap = v.capacity(); // ask for v's capacity (same as N)
    bool is_empty = v.empty(); // ask whether a cx_vector is empty
    bool is_full = v.full(); // ask whether a cx_vector is full
    bool equal = v == v; // compare two cx_vectors (of the same type)
    v.clear() // clear a cx_vector

    // we can use some of the usual functions
    v.push_back(T{});
    T& t1 = v[0];
    T& t2 = v.back();
    T t = v.pop_back();

    // and use iterators:
    // begin and end
    // cbegin and cend
    // rbegin and rend
    // crbegin and crend
    // (therefore also range-for loops)

    // we can have compile-time access with get
    T& t3 = get<0>(v);

    // and we can use resize_and_overwrite for efficient initialization
    resize_and_overwrite(v,
      [] (T* data, std::size_t sz) -> std::size_t {
        // copy Ts to data and return the new size
        return 0;
      });
  }
};
Note
capacity is always available as constexpr, even though v above is a function parameter and therefore not constexpr.

A cx_vector may also be initialized with CTAD:

// v is a cx_vector<int, 3>
auto v = cx_vector{1, 2, 3};

for_each_n_args.hpp

for_each_n_args.hpp provides a method for calling a function (or other callable) with batches of arguments from a parameter pack.

Examples:

auto f(int x, int y) -> void { /* do something with x and y */ }
stdx::for_each_n_args(f, 1, 2, 3, 4); // this calls f(1, 2) and f(3, 4)

The number of arguments passed to for_each_n_args must be a multiple of the argument "batch size" - which by default is the arity of the passed function.

Sometimes, the passed callable is a generic function where the arity cannot be automatically determined, or sometimes it may be a function with default arguments which we want to use. In that case it is possible to override the default batch size:

auto f(auto x, auto y) -> void { /* do something with x and y */ }
stdx::for_each_n_args<2>(f, 1, 2, 3, 4); // this calls f(1, 2) and f(3, 4)

auto g(int x, int y, int z = 42) -> void { /* do something with x, y and z */ }
stdx::for_each_n_args<2>(g, 1, 2, 3, 4); // this calls g(1, 2, 42) and g(3, 4, 42)

function_traits.hpp

function_traits.hpp contains type traits for introspecting function signatures. It works with functions, lambda expressions, and classes with operator().

Examples:

auto f1() -> void {}
using f1_return = stdx::return_t<decltype(f1)>;         // void
using f1_args = stdx::args_t<decltype(f1), std::tuple>; // std::tuple<>

auto f2(int) -> int { return 0; }
using f2_return = stdx::return_t<decltype(f2)>;         // int
using f2_args = stdx::args_t<decltype(f2), std::tuple>; // std::tuple<int>

auto l = [] (int) -> int { return 0; };
using l_return = stdx::return_t<decltype(l)>;         // int
using l_args = stdx::args_t<decltype(l), std::tuple>; // std::tuple<int>

stdx::args_t returns a list of the function arguments. std::decayed_args_t returns the same list, but with std::decay_t applied to each element. This is useful for example when you need to copy and store a tuple of the arguments.

auto f(int&, std::string&) -> void {}
using f_args = stdx::decayed_args_t<decltype(f), std::tuple>; // std::tuple<int, std::string>

stdx::arity_t returns the arity of a function (as a std::integral_constant). stdx::arity_v provides convenient access to the value.

auto f(int&, std::string&) -> void {}
using f_arity_t = stdx::arity_t<decltype(f)>; // std::integral_constant<std::size_t, 2>
constexpr auto f_arity_v = stdx::arity_v<decltype(f)>;

stdx::nth_arg_t returns a function parameter type at a given index. stdx::decayed_nth_arg_t is the equivalent with std::decay_t applied.

auto f(int&) -> void {}
using first_arg_t = stdx::nth_arg_t<decltype(f), 0>; // int&
using first_decayed_arg_t = stdx::decayed_nth_arg_t<decltype(f), 0>; // int
Note
Function traits work on functions (and function objects): not function templates or overload sets. For instance therefore, they will not work on generic lambda expressions.

The one exception to this is stdx::arity_t - it works on generic lambdas.

auto l = [](auto, auto) -> void {};
using f_arity_t = stdx::arity_t<decltype(l)>; // std::integral_constant<std::size_t, 2>
constexpr auto f_arity_v = stdx::arity_v<decltype(l)>;

functional.hpp

Note
P2714 added the ability (in C++26) to use a non-type template parameter for the bound function; this works for function pointers in C++17, and also for lambda expressions in C++20 and beyond.

with_result_of

with_result_of is a class that can be used for lazy evaluation. with_result_of wraps a callable (often a lambda expression) and can implicitly convert to the return type of the callable. It may be passed to functions that perfectly forward their arguments - a good example is an emplace function on a container - and the conversion happens only when the required value is actually used.

// S is a type that is some work to construct
// so we use a maker function
struct S { ... };
auto make_S() -> S;

std::unordered_map<int, S> m;

v.emplace(0, make_S()); // this works, but incurs a temporary construct, move and destruct
v.emplace(0, stdx::with_result_of{make_S}); // this constructs S in-place thanks to RVO

with_result_of can help to achieve in-place construction, effectively by deferring evaluation of function arguments.

intrusive_forward_list.hpp

intrusive_forward_list is a singly-linked list designed for use at compile-time or with static objects. It supports pushing and popping at the front or back.

// A node in an intrusive_list must have a next pointer
struct node {
  node *next{};
};

stdx::intrusive_forward_list<node> l;

node n1{};
l.push_front(&n1);

node n2{};
l.push_back(&n2);

node n3{};
l.push_back(&n3);

node* nf = l.pop_front();

l.clear();
bool b = l.empty();

intrusive_forward_list supports the same node validation policy arguments as intrusive_list.

intrusive_list.hpp

intrusive_list is a doubly-linked list designed for use at compile-time or with static objects. It supports pushing and popping at the front or back, forward iteration, and insertion and removal from the middle.

// A node in an intrusive_list must have prev and next pointers
struct node {
  node *prev{};
  node *next{};
};

stdx::intrusive_list<node> l;

node n1{};
l.push_front(&n1);

node n2{};
l.push_back(&n2);

node n3{};
auto i = std::find_if(std::begin(l), std::end(l), /* some predicate */);
l.insert(i, &n3); // insertion into the middle is constant-time

l.remove(&n2); // removal from the middle is constant-time
node* nf = l.pop_front();
node* nb = l.pop_back();

l.clear();
bool b = l.empty();

Node validity checking

An intrusive_list has a second template parameter which is whether to operate with or without node validity checking. The default is stdx::node_policy::checked.

// The second template argument here is the default
stdx::intrusive_list<node, stdx::node_policy::checked> l;

This means that:

  • nodes to be inserted/pushed must have prev and next pointers equal to nullptr

  • node prev and next pointers are cleared on removal/pop

Note
The second item here means that clear is a linear-time operation with stdx::node_policy::checked. For faster but less safe operation, stdx::node_policy::unchecked is available.

If the validity policy is violated (for example by attempting to push a node whose pointers are already populated) the result is a panic.

iterator.hpp

iterator.hpp contains ct_capacity, a constexpr function that returns the capacity of a container which is known at compile-time.

auto const a = std::array{1, 2, 3, 4};
constexpr auto c = stdx::ct_capacity(a); // std::size_t{4}

ct_capacity can be called with:

  • std::array

  • std::span (unless it has std::dynamic_extent)

  • stdx::span (unless it has stdx::dynamic_extent)

  • stdx::cx_map

  • stdx::cx_multimap

  • stdx::cx_queue

  • stdx::cx_set

  • stdx::cx_vector

ct_capacity_v is a corresponding variable template of type std::size_t.

memory.hpp

memory.hpp contains one thing:

numeric.hpp

numeric.hpp provides an implementation of some numeric algorithms.

transform_reduce and transform_reduce_n

stdx::transform_reduce is similar to std::transform_reduce, but variadic in its inputs.

template <typename T, typename InputIt, typename ROp, typename TOp,
          typename... InputItN>
constexpr auto transform_reduce(InputIt first, InputIt last, T init,
                                ROp rop, TOp top, InputItN... first_n) -> T;

stdx::transform_reduce is to std::transform_reduce as stdx::transform is to std::transform.

Note
The return type of stdx::transform_reduce is the first template parameter, which allows it to be manifestly fixed more conveniently to avoid a common pitfall with accumulation algorithms, viz. summing a range of double​s and passing 0 - an int - as the initial value.
Caution
Unlike stdx::transform, the return type here does not include the iterators: this means stdx::transform_reduce violates the Law of Useful Return (just like std::transform_reduce). However, it is probably true that stdx::transform_reduce is mostly used just for the accumulation result, where stdx::transform writes to an output iterator.
Note
Like stdx::transform, stdx::transform_reduce is constexpr in C++20 and later, because it uses std::invoke.

stdx::transform_reduce_n is just like stdx::transform_reduce, but instead of taking two iterators to delimit the input range, it takes an iterator and size.

template <typename T, typename InputIt, typename Size, typename ROp,
          typename TOp, typename... InputItN>
constexpr auto transform_reduce_n(InputIt first, Size n, T init, ROp rop,
                                  TOp top, InputItN... first_n) -> T;

optional.hpp

optional.hpp provides an implementation that mirrors <optional>, but uses tombstone_traits instead of a bool variable.

Here is a problem with std::optional:

enum struct S1 : std::uint8_t { /* ... values ... */ };
static_assert(sizeof(S1) == 1);
static_assert(sizeof(std::optional<S1> == 2);

enum struct S2 : std::uint16_t { /* ... values ... */ };
static_assert(sizeof(S2) == 2);
static_assert(sizeof(std::optional<S2>) == 4);

enum struct S3 : std::uint32_t { /* ... values ... */ };
static_assert(sizeof(S3) == 4);
static_assert(sizeof(std::optional<S3>) == 8);

std::optional basically stores a bool flag as well as the type. Because of size and alignment constraints, std::optional of a small type typically ends up being double the size of the type. Much of the time, we aren’t using all the bits in the type - especially if it’s an enumeration - so there is probably some sentinel value that we can treat as "invalid". AKA a "tombstone".

This is where tombstone_traits come in: specializing stdx::tombstone_traits allows us to specify that sentinel value and avoid storing an extra bool.

Note
The name "tombstone" arises from use in hash maps where it is used to signal a "dead" object.
enum struct S : std::uint8_t { /* ... values ... */ };

template <> struct stdx::tombstone_traits<S> {
    // "-1" is not a valid value
    constexpr auto operator()() const {
        return static_cast<S>(std::uint8_t{0xffu});
    }
};

static_assert(sizeof(S) == 1);
static_assert(sizeof(stdx::optional<S> == 1);

To use stdx::optional, specialize stdx::tombstone_traits for the required type, giving it a call operator that returns the sentinel value. After that, stdx::optional’s interface mirrors that of `std::optional. The C+​+23 monadic operations on std::optional are available on stdx::optional with C++17.

Why a call operator and not just a static value? To deal with move-only (and even non-movable) types.

Note
Like std::optional, stdx::optional can be constructed with std::nullopt or with std::in_place. (stdx does not redefine these types.)
Note
stdx::optional does not use exceptions. There is no stdx::bad_optional_access. If you access a disengaged stdx::optional, you will get the tombstone value!

Tombstone values

Instead of specializing stdx::tombstone_traits, sometimes it’s easier (especially for integral or enumeration types) to provide the tombstone value inline. We can do this with stdx::tombstone_value.

auto o = stdx::optional<int, stdx::tombstone_value<-1>>{};
Caution
Don’t specialize tombstone_traits for the builtin integral types - that’s risky if the definition is seen more widely. Instead use a stdx::tombstone_value where needed.
Note
The default tombstone_traits for floating-point types have infinity as the tombstone value. At first thought, NaN is the obvious tombstone, but NaNs never compare equal to anything, not even themselves.

Multi-argument transform

stdx::optional provides one extra feature over std::optional: the ability to call transform with multiple arguments. C++23 transform is a member function on stdx::optional too, but stdx::transform exists also as a free function on stdx::optional.

// S is a struct with tombstone_traits that contains an integer value

auto opt1 = stdx::optional<S>{17};
auto opt2 = stdx::optional<S>{42};
auto opt_sum = transform(
    [](S const &x, S const &y) { return S{x.value + y.value}; },
    opt1, opt2);

This flavor of transform returns the result of the function only if all of its stdx::optional arguments are engaged. If any one is not, a disengaged stdx::optional is returned.

panic.hpp

panic("reason") is a function that is used in emergencies: something fundamental went wrong (e.g. a precondition was violated) and there is no good way to recover. It’s like assert except that the behaviour of panic can be overridden.

A panic_handler is a struct that exposes a static panic method. That method may have two overloads: one that takes a ct_string template argument (as well as more runtime arguments), and another that takes only runtime arguments.

The default panic handler does nothing; to override that behaviour, provide a custom panic handler and specialize the variable template stdx::panic_handler, like this for example:

struct custom_panic_handler {
  static auto panic(auto&&... args) noexcept -> void {
    // log args and then...
    std::terminate();
  }

template <stdx::ct_string S>
  static auto panic(auto&&... args) noexcept -> void {
    // log args (including the compile-time string) and then...
    std::terminate();
  }
};

template <> inline auto stdx::panic_handler<> = custom_panic_handler{};

When something inside stdx goes wrong, the panic handler’s panic function will be called.

Note
stdx will always call panic with a compile-time string if possible (C++20), or with a single char const * if not. You are free to use panic with a logging framework to provide a "fatal" log function; in that case any arguments you pass through will be passed to panic and presumably handled by your choice of logging.

priority.hpp

priority_t<N> is a class that can be used for easily selecting complex function overloads. priority_t<0> is the lowest priority. priority<N> is a value of type priority_t<N>.

template </*some strong constraint*/ T>
auto f(T t, stdx::priority_t<2>) {
  // highest priority: call this function if possible
}

template </*some weaker/less preferred constraint*/ T>
auto f(T t, stdx::priority_t<1>) {
  // call this function if the highest-priority overload can't be called
}

template <typename /*no constraint*/ T>
auto f(T t, stdx::priority_t<0>) {
  // fallback to this function if both higher priority overloads don't fit
}

// at the call site, use the highest priority
auto result = f(t, stdx::priority<2>);

ranges.hpp

ranges.hpp contains a single concept: range. A type models the stdx::range concept if std::begin and std::end are defined for that type.

rollover.hpp

rollover.hpp provides a class template rollover_t that is intended to act like an unsigned integral type, but with semantics that include the ability to roll-over on overflow.

A rollover_t can be instantiated with any unsigned integral type:

// explicit type
auto x = stdx::rollover_t<std::uint8_t>{};

// deduced type: must be unsigned
auto y = stdx::rollover_t{1u}; // rollover_t<unsigned int>

It supports all the usual arithmetic operations (+ - * / %) and behaves much like an unsigned integral type, with defined overflow and underflow semantics.

Comparison semantics

rollover_t supports equality, but the comparison operators (< <​= > >=) are deleted. Instead, cmp_less is provided, with different semantics. A rollover_t considers itself to be in the middle of a rolling window where half the bit-range is always lower and half is higher.

For instance, imagine a 3-bit unsigned integral type. There are eight values of this type: 0 1 2 3 4 5 6 7. Let’s call the rollover_t over this type R.

Caution
operator< on rollover_t is not antisymmetric!

For any value, there are always four values (half the bit-space) less than it, and four values greater than it. And of course it is equal to itself. e.g. for the R value 5:

  • 1, 2, 3, 4 are all less than 5

  • 6, 7, 0, 1 are all greater than 5

i.e. cmp_less(R{1u}, R{5u}) is true. And cmp_less(R{5u}, R{1u}) is true.

Effectively any value partitions the cyclic space in this way.

Caution
operator< on rollover_t is not transitive!

Also, the following are all true for R:

  • 1 < 3

  • 3 < 5

  • 5 < 7

  • 7 < 1

The cyclic nature of the space means that operator< is neither antisymmetric nor transitive! (Lack of antisymmetry might be viewed as a special case of non-transitivity.)

This means we need to take care with operations that assume the antisymmetric and transitive nature of the less-than operation. In particular cmp_less does not define a strict weak order — which is why operator< and friends are deleted. In the absence of data constraints, rollover_t cannot be sorted with std::sort.

Note
A suitable constraint might be that the data lies completely within half the bit-range: in that case, cmp_less would have the correct properties and could be used as a comparator argument to std::sort. As always in C++, we protect against Murphy, not Machiavelli.

Use with std::chrono

rollover_t is intended for use in applications like timers which may be modelled as a 32-bit counter that rolls over. In that case, it makes sense to consider a sliding window centred around "now" where half the bit-space is in the past, and half is in the future. Under such a scheme, in general it is undefined to schedule an event more than 31 bits-worth in the future.

// 32-bit rollover type
using ro_t = stdx::rollover_t<std::uint32_t>;
// Used with a microsecond resolution
using ro_duration_t = std::chrono::duration<ro_t, std::micro>;
using ro_time_point_t = std::chrono::time_point<std::chrono::local_t, ro_duration_t>;

This allows us to benefit from the typed time handling of std::chrono, and use cmp_less for specialist applications like keeping a sorted list of timer tasks, where we have the constraint that we never schedule an event beyond a certain point in the future.

span.hpp

span.hpp provides an implementation of span.

static_assert.hpp

STATIC_ASSERT is a way to produce compile-time errors using formatted strings.

template <typename T>
constexpr auto f() {
  STATIC_ASSERT(std::is_integral<T>,
                "f() must take an integral type, received {}", CX_VALUE(T));
}

f<float>(); // produces compile-time error

The arguments to be formatted (if any) must be wrapped in CX_VALUE if they are not admissible as template arguments.

The output from this (which varies by compiler) will contain the formatted string, and could be something like:

main.cpp:14:27: error: no matching member function for call to 'emit'
...
include/stdx/static_assert.hpp:16:18: note: because
'stаtiс_аssert<ct_string<47>{{"f() must take an integral type, received float"}}>' evaluated to false
   16 |         requires stаtiс_аssert<S>
      |                  ^
Note
clang produces these "string-formatted" errors from version 15 onwards; GCC produces them from version 13.2 onwards.

tuple.hpp

tuple.hpp provides a tuple implementation that mirrors std::tuple. Mostly, stdx::tuple works the same way as std::tuple, with some extra functionality and faster compilation times. All functions on tuples are constexpr-capable.

Note
tuple is available only in C++20 and later.

Element types and size of a tuple

As with std::tuple, we can ask for the compile-time elements and size of a stdx::tuple using tuple_element_t and tuple_size_v.

template <typename T>
constexpr auto tuple_size_v = /* implementation */;
template <std::size_t I, typename T>
using tuple_element_t = /* implementation */;

The standard provides std::tuple_element and std::tuple_size as types; in stdx, only tuple_element_t and tuple_size_v are provided, since they are the most frequently used constructs. If needed, types can be synthesized from them, but if not, it’s quicker to compile just an alias and a variable template.

Constructing a tuple

A tuple can be constructed either with CTAD, or with make_tuple, or with forward_as_tuple.

auto t = stdx::tuple{1, 2};            // tuple<int, int>
auto t = stdx::make_tuple(1, 2);       // the same
auto t = stdx::forward_as_tuple(1, 2); // tuple<int&&, int&&>

make_tuple decays its arguments, just like std::make_tuple. However, std::make_tuple also does std::reference_wrapper-unwrapping, which stdx::make_tuple does not.

forward_as_tuple creates a tuple of forwarded references, like std::forward_as_tuple.

Accessing tuple elements

The ordinary way to access tuple elements is to use get, as with a std::tuple:

auto t = stdx::tuple{1, true}; // tuple<int, bool>
auto x_by_index = get<0>(t); // int
auto y_by_index = get<1>(t); // bool
auto x_by_type = get<int>(t); // int
auto y_by_type = get<bool>(t); // bool
Note
stdx::get is accessed here by argument-dependent lookup.

Like std::get, stdx::get with a type will be ambiguous if that type is in the tuple multiple times.

Unlike std::get, stdx::get is "SFINAE-friendly". If stdx::get causes a function template instantiation to be ill-formed, it will not cause an error, but instead will cause that function to be silently removed from the overload set candidates. With stdx::get, Substitution Failure Is Not An Error.

For access by index, we can also use indexing syntax:

using namespace stdx::literals;
auto t = stdx::tuple{1, true};
auto x_by_index1 = t[index<0>];
auto x_by_index2 = t[0_idx]; // equivalent

_idx is a user-defined literal in the stdx::literals namespace. It is equivalent to using the index variable template.

Note
All forms of access preserve the value category of the tuple; i.e. accessing an int member of a stdx::tuple const & gives an int const & and so on.

Comparing tuples

Comparing one stdx::tuple with another works the same way as with std::tuple.

Member functions on a tuple

apply can be used like std::apply, but is a member function.

auto t = stdx::tuple{1, 2};
auto sum = t.apply([] (auto... args) { return (args + ... + 0); }); // 3

apply is also available as a free function in tuple_algorithms.hpp.

fold_left and fold_right can be used to fold over a tuple and compute a reduction. They both take an initial value for the fold as the first argument, and a binary reduction function as the second.

auto t = stdx::tuple{1, 2, 3};
auto sum1 = t.fold_left(0, std::plus{});  // 6
auto sum2 = t.fold_right(0, std::plus{}); // also 6

Here, fold_left computes a left-fold, i.e. (​(​(0 + 1) + 2) + 3). fold_right computes the right-fold (0 + (1 + (2 + 3))).

Both forms of fold can perform computations in type-space as well as value-space. That is, the return type of the binary function passed to the fold can depend on the arguments. In this way a fold can build up a type dependent on what is in the tuple.

auto t1 = stdx::tuple{1, 2, 3};
auto t2 = t.fold_left(
  stdx::tuple{},
  [] (auto so_far, auto y) { return tuple_cat(so_far, stdx::tuple{y}); });
// t1 and t2 are the same, but intermediate types in the fold varied
Note
tuple_cat is an algorithm in tuple_algorithms.hpp.

join is a member function that works the same way as fold_left, but without needing an initial value. It has overloads either for non-empty tuples, or for empty tuples with a given default.

auto sum1 = stdx::tuple{1, 2, 3}.join(std::plus{});  // 6
auto sum2 = stdx::tuple{1}.join(std::plus{});        // 1
auto sum3 = stdx::tuple{}.join(42, std::plus{});     // 42

join is useful for things like string concatenation with comma separation.

auto sum1 = stdx::tuple{"hello"s, "world"s}.join(
    [] (auto const &acc, auto const &s) { return acc + ", " + s; });  // "hello, world"

Indexed tuples

Sometimes, it is useful to index a tuple by something other than a plain std::size_t or a type. A tuple can act as a sort of map by indexing on multiple types, for instance. That’s the job of indexed_tuple.

template <typename Key, typename Value> struct map_entry {
  using key_t = Key;
  using value_t = Value;
  value_t value;
};
template <typename T> using key_for = typename T::key_t;

struct X;
struct Y;
auto t = stdx::make_indexed_tuple<key_for>(map_entry<X, int>{42},
                                           map_entry<Y, int>{17});
auto x = get<X>(t).value; // 42
auto y = get<Y>(t).value; // 17

Notice a few things here:

  • X and Y are tag types; declared only and not defined.

  • make_indexed_tuple takes a number of type functions (here just key_for) that define how to look up elements.

  • get is working not with a std::size_t index or the actual type contained within the tuple, but with the tag type that will be found by key_for.

A regular (unindexed) tuple can be converted to an indexed_tuple using apply_indices to add type-indexing functions:

// with definitions as above
auto t = stdx::tuple{map_entry<X, int>{42}}; // regular tuple
auto i = stdx::apply_indices<key_for>(t);    // tuple indexed with key_for
auto x = get<X>(i).value;                    // 42

one_of

one_of is a convenient way to declaratively determine if a value is in a set.

auto is_taxicab(int x) -> bool {
  return x == one_of{2, 1'729, 875'339'319};
}
Note
one_of is a type, not a function.

tuple_algorithms.hpp

tuple_algorithms.hpp contains various (free function) algorithms that work on stdx::tuple.

Summary of tuple algorithms

  • all_of, any_of, none_of - like the standard versions, but over a tuple

  • apply - like std::apply, but also a member function on tuple

  • cartesian_product - create a tuple-of-tuples-of-references that is the cartesian product of the inputs

  • cartesian_product_copy - create a tuple-of-tuples that is the cartesian product of the inputs

  • chunk_by - split a tuple into a tuple-of-tuples according to a type function

  • contains_type - a variable template that is true when a tuple contains a given type

  • enumerate - like for_each, but including a compile-time index

  • filter - for compile-time filtering

  • for_each - like the standard version, but over a tuple

  • fold_left and fold_right - member functions on tuple

  • join - a member function on tuple, like fold_left but without an initial value

  • sort - sort a tuple by a function on the contained types

  • to_ordered_set - produce a tuple of unique types that are sorted

  • to_unordered_set - produce a tuple of unique types that are in the order given

  • transform - a variadic transform on tuple(s)

  • tuple_cat - like std::tuple_cat

  • tuple_cons - add an element to the front of a tuple

  • tuple_push_back - alias for tuple_snoc

  • tuple_push_front - alias for tuple_cons

  • tuple_snoc - add an element to the back of a tuple

  • unique - produce a tuple where adjacent types that are the same are merged into one element (the first such)

all_of, any_of, none_of

all_of, any_of and none_of work in the same way as the standard versions on ranges, but over a tuple instead.

auto t = stdx::tuple{1, 2, 3};
auto x = stdx::any_of([](auto n) { return n % 2 == 0; }, t); // true

apply

See member functions. stdx::apply is also available as a free function, for compatibility with std::apply.

auto t = stdx::tuple{1, 2, 3};
auto sum = stdx::apply([] (auto... args) { return (args + ... + 0); }, t); // 6

stdx::apply can also be called with a variadic pack of tuples, which are unpacked and passed to the function:

auto t1 = stdx::tuple{1, 2, 3};
auto t2 = stdx::tuple{4, 5, 6};
auto sum = stdx::apply([] (auto... args) { return (args + ... + 0); }, t1, t2); // 21

cartesian_product

cartesian_product takes any number of tuples and returns the tuple-of-tuples that is the cartesian product of the members. Each returned tuple is a tuple of references.

auto t1 = stdx::tuple{1, 2};
auto t2 = stdx::tuple{'a', 'b'};
auto c = stdx::cartesian_product(t1, t2);
// produces {{1, 'a'}, {1, 'b'}, {2, 'a'}, {2, 'b'}}
Note
The cartesian product of no tuples is a tuple containing an empty tuple.

cartesian_product_copy

The same as cartesian_product, but the returned tuples have values, not references.

Note
This can be useful for constexpr applications: in general one cannot take the address of a local constexpr variable unless it is static.

chunk_by

chunk_by takes a tuple and returns a tuple-of-tuples, where each tuple is grouped by type name.

auto t = stdx::tuple{1, 2, 3, true, false}; // tuple<int, int, int, bool, bool>
auto c1 = stdx::chunk_by(t); // tuple<tuple<int, int, int>, tuple<bool, bool>>
auto c2 = stdx::chunk(t);    // without a template argument, the same as chunk_by

Notice that chunk_by doesn’t sort the tuple first; it only chunks elements that are adjacent.

auto t = stdx::tuple{1, true, 3}; // tuple<int, bool, int>
auto c = stdx::chunk_by(t);      // tuple<tuple<int>, tuple<bool>, tuple<int>>

chunk_by takes an optional template argument which is a type function (a template of one argument). This will be applied to each type in the tuple to obtain a type name that is then used to chunk. By default, this type function is std::type_identity_t.

contains_type

contains_type is a variable template that is true when a tuple contains a given type.

using T = stdx::tuple<int, bool, int &>;
static_assert(stdx::contains_type<T, int>);

It also works on indexed tuples.

// see "Indexed tuples"
using T = stdx::indexed_tuple<stdx::detail::index_function_list<key_for>,
                              map_entry<X, int>, map_entry<Y, int>>;
static_assert(stdx::contains_type<T, X>);

If contains_type<Tuple, Type> is true, then you can use get<Type> to retrieve the appropriate member (assuming the type is contained exactly once).

enumerate

enumerate runs a given function object on each element of a tuple in order. Like for_each, it is variadic, taking an n-ary function and n tuples. The operator() of the given function object takes the std::size_t index (zero-based, of course) as an NTTP.

auto t = stdx::tuple{1, 2, 3};
stdx::enumerate([] <auto Idx> (auto x) { std::cout << Idx << ':' << x << '\n'; }, t);
Note
Like for_each, enumerate returns the function object passed to it.

enumerate is also available for std::array, but to be explicit it is called unrolled_enumerate:

auto a = std::array{1, 2, 3};
stdx::unrolled_enumerate([] <auto Idx> (auto x) { std::cout << Idx << ':' << x << '\n'; }, a);

filter

filter allows compile-time filtering of a tuple based on the types contained.

auto t = stdx::tuple{
  std::integral_constant<int, 1>{}, std::integral_constant<int, 2>{},
  std::integral_constant<int, 3>{}, std::integral_constant<int, 4>{}};

template <typename T>
using is_even = std::bool_constant<T::value % 2 == 0>;

auto filtered = stdx::filter<is_even>(t);
// filtered is a stdx::tuple<std::integral_constant<int, 2>,
//                           std::integral_constant<int, 4>>
Note
filtering a tuple can only be done on the types, not on the values! The type of the filtered result must obviously be known at compile time. However, the values within the tuple are also preserved.

for_each

for_each runs a given function on each element of a tuple in order. Like transform, it is variadic, taking an n-ary function and n tuples.

auto t = stdx::tuple{1, 2, 3};
stdx::for_each([] (auto x) { std::cout << x << '\n'; }, t);
Note
Like std::for_each, stdx::for_each returns the function object passed to it. This can be useful for stateful function objects.

for_each is also available for std::array, but to be explicit it is called unrolled_for_each:

auto a = std::array{1, 2, 3};
stdx::unrolled_for_each([] (auto x) { std::cout << x << '\n'; }, a);

gather_by

gather_by takes a tuple and returns a tuple-of-tuples, where each tuple is grouped by type name.

auto t = stdx::tuple{1, true, 2, false, 3}; // tuple<int, int, int, bool, bool>
auto c1 = stdx::gather_by(t); // tuple<tuple<int, int, int>, tuple<bool, bool>>
auto c2 = stdx::gather(t);    // without a template argument, the same as gather_by

gather_by is like chunk_by, except that gather_by gathers elements that are not adjacent.

gather_by takes an optional template argument which is a type function (a template of one argument). This will be applied to each type in the tuple to obtain a type name that is then used to chunk. By default, this type function is std::type_identity_t.

Warning
gather_by uses sort - not stable_sort! For a given type, the order of values in the gathered tuple is not necessarily the same as that of the input tuple.

sort

sort is used to sort a tuple by type name.

auto t = stdx::tuple{42, true}; // tuple<int, bool>
auto s = stdx::sort(t);         // tuple<bool, int> {true, 42}

Like chunk_by, sort takes an optional template argument which is a type function (a template of one argument). This will be applied to each type in the tuple to obtain a type name that is then sorted alphabetically. By default, this type function is std::type_identity_t.

Warning
sort is not stable_sort! For a given type, the order of values in the sorted tuple is not necessarily the same as that of the input tuple.

to_sorted_set

to_sorted_set is sort followed by unique: it sorts the types in a tuple, then collapses it so that there is only one element of each type.

auto t = stdx::tuple{1, true, 2, false};
auto u = stdx::to_sorted_set(t); // {<some bool>, <some integer>}
Warning
sort is not stable_sort! The value in the example above is not necessarily {true, 1} because there is no stable ordering between elements of the same type.

to_unsorted_set

to_unsorted_set produces a tuple of unique types in the same order as the original tuple. In each case the value of that type is the first one in the original tuple.

auto t = stdx::tuple{1, true, 2, false};
auto u = stdx::to_unsorted_set(t); // {1, true}

transform

transform is used to transform the values (and potentially the types) in one tuple, producing another.

auto t = stdx::tuple{1, 2, 3};
auto u = stdx::transform([](auto x) { return x + 1; }, t); // {2, 3, 4}

transform is not limited to working on a single tuple: given an n-ary function and n tuples, it will do the correct thing and "zip" the tuples together:

auto t1 = stdx::tuple{1, 2, 3};
auto t2 = stdx::tuple{2, 3, 4};
auto u = stdx::transform(std::multiplies{}, t1, t2); // {2, 6, 12}
Note
It’s OK to zip together different length tuples: transform will produce a tuple that is the length of the shortest input.

transform can also apply indexing functions while it transforms:

// see "Indexed tuples"
struct X;
auto t = stdx::transform<key_for>(
  [](auto value) { return map_entry<X, int>{value}; },
  stdx::tuple{42});
auto x = get<X>(t).value; // 42

tuple_cat

tuple_cat works just like std::tuple_cat.

tuple_cons/tuple_push_front

tuple_cons adds an item to the front of a tuple. tuple_push_front is an alias for tuple_cons.

auto t = stdx::tuple_cons(1, stdx:tuple{2, 3}); // {1, 2, 3}
Note
tuple_cons preserves the reference qualifiers in the given tuple, but decays the "single" argument, as make_tuple does.

tuple_snoc/tuple_push_back

tuple_snoc adds an item to the back of a tuple. tuple_push_back is an alias for tuple_snoc.

auto t = stdx::tuple_snoc(stdx:tuple{2, 3}, 1); // {2, 3, 1}
Note
tuple_snoc preserves the reference qualifiers in the given tuple, but decays the "single" argument, as make_tuple does.

unique

unique works like std::unique, but on types rather than values. i.e. unique will collapse adjacent elements whose type is the same. The first such element is preserved in the result.

auto t = stdx::tuple{1, 2, true};
auto u = stdx::unique(t); // {1, true}

tuple_destructure.hpp

tuple_destructure.hpp allows the use of structured bindings with stdx::tuple.

auto t = stdx::tuple{1, 2};
auto &[x, y] = t;
Note
tie is not implemented.

type_traits.hpp

type_traits.hpp contains a few things from the standard:

always_false_v

always_false_v is a variable template that can be instantiated with any number of type arguments and always evaluates to false at compile-time. This is useful for writing static_assert where it must depend on types (at least before C++23 and P2593).

template <typename T>
auto f(T) {
  if constexpr (std::integral<T>) {
  } else {
    // doesn't work before C++23
    // static_assert(false, "S must be instantiated with an integral type");

    // instead, this works
    static_assert(stdx::always_false_v<T>, "S must be instantiated with an integral type");
  }
};

apply_sequence

A type_list or a value_list can be unpacked and passed as individual template arguments with apply_sequence. A function object whose call operator is a variadic function template with no runtime arguments is called with the pack of arguments.

using L1 = stdx::type_list<std::integral_constant<int, 1>,
                           std::integral_constant<int, 2>>;
int x = stdx::apply_sequence<L1>([&] <typename... Ts> () { return (0 + ... + Ts::value); });
// x is 3

using L2 = stdx::value_list<1, 2>;
int y = stdx::apply_sequence<L1>([&] <auto... Vs> () { return (0 + ... + Vs); });
// y is 3

apply_sequence can also be used with a std::integer_sequence:

using L3 = stdx::make_index_sequence<3>;
auto y = stdx::apply_sequence<L3>([&] <auto... Vs> () { y += V; });
// y is 3
Note
If the function iterates the pack by folding over operator, then template_for_each is probably what you want.

is_function_object_v

is_function_object_v is a variable template that detects whether a type is a function object, like a lambda. It is true for generic lambdas, too.

auto f() {};
auto const lam = []{};
auto const gen_lam = []<typename>(){};

stdx::is_function_object_v<decltype(f)>;         // false
stdx::is_function_object_v<decltype(lam)>;       // true
stdx::is_function_object_v<decltype(gen_lam)>;   // true

is_same_unqualified_v

is_same_unqualified_v is a variable template that detects whether a two types are the same are removing top-level cv-qualifications and references, if any.

stdx::is_same_unqualified_v<int, int const&>; // true
stdx::is_same_unqualified_v<int, void>;       // false

is_specialization_of_v

is_specialization_of_v is a variable template that detects whether a type is a specialization of a given template.

using O = std::optional<int>;

stdx::is_specialization_of_v<O, std::optional>;   // true
stdx::is_specialization_of_v<int, std::optional>; // false

is_specialization_of_v is suitable for templates with type parameters only (not template-template parameters or NTTPs). For templates with value parameters, use is_value_specialization_of_v.

template <auto N> struct S;
using T = S<0>;

stdx::is_value_specialization_of_v<T, S>; // true
Note
is_type_specialization_of_v is a synonym for is_specialization_of_v.

is_specialization_of is a function that can be used either way.

using O = std::optional<int>;
template <auto N> struct S;
using T = S<0>;

stdx::is_specialization_of<O, std::optional>(); // true
stdx::is_specialization_of<T, S>();             // true
Note
Perhaps until C++ has universal template parameters, there is no easy way to write this function where the template takes a mixture of type, value, and template parameters. So this is useful for templates whose parameters are all types or all values.

is_structural_v

is_structural_v<T> is true when T is a structural type suitable for use as a non-type template parameter.

static_assert(stdx::is_structural_v<int>);
static_assert(not stdx::is_structural_v<std::string>);
Note
Detecting structurality of a type is not yet possible in the general case, so there are certain structural types for which this trait will be false. In practice those types should be rare, and there should be no false positives.

template_for_each

A type_list or a value_list can be iterated with template_for_each. A function object whose call operator is a unary function template with no runtime arguments is called with each element of the list.

using L1 = stdx::type_list<std::integral_constant<int, 1>,
                           std::integral_constant<int, 2>>;
int x{};
stdx::template_for_each<L1>([&] <typename T> () { x += T::value; });
// x is now 3

using L2 = stdx::value_list<1, 2>;
int y{};
stdx::template_for_each<L2>([&] <auto V> () { y += V; });
// y is now 3

template_for_each can also be used with a std::integer_sequence:

using L3 = stdx::make_index_sequence<3>;
std::size_t y{};
stdx::template_for_each<L3>([&] <auto V> () { y += V; });
// y is now 3
Note
A primary use case of template_for_each is to be able to use a list of tag types without those types having to be complete.

type_or_t

type_or_t is an alias template that selects a type based on whether or not it passes a predicate. If not, a default is returned.

using A = int *;
using T = stdx::type_or_t<std::is_pointer, A>;        // A

using B = int;
using X = stdx::type_or_t<std::is_pointer, B>;        // void (implicit default)
using Y = stdx::type_or_t<std::is_pointer, B, float>; // float (explicit default)

type_list and value_list

type_list is an empty struct templated over any number of types. value_list is an empty struct templated over any number of NTTPs.

udls.hpp

Note
Taking a cue from the standard, all UDLs in stdx are declared in inline namespaces - typically stdx::literals.

Useful user-defined literals

udls.hpp contains a few handy user-defined literals so that code using boolean or small index values can be more expressive at the call site than just using bare true, false, or 0 through 9. This also makes it safer to use templates with bool or integral parameters.

using namespace stdx::literals;

template <bool X>
struct my_type { ... };

using my_type_with_X = my_type<"X"_true>;
using my_type_without_X = my_type<"X"_false>;

using my_type_with_X_alt = my_type<"X"_b>;
using my_type_without_X_alt = my_type<not "X"_b>;

auto t = stdx::tuple{1, true, 3.14f};
auto value = get<"X"_1>(t); // true
Note
The _N literals each return a std::integral_constant<std::size_t, N>. This is implicitly convertible to a std::size_t.

There are also some UDLs that are useful when specifying sizes in bytes:

using namespace stdx::literals;

// decimal SI prefixes
constexpr auto a = 1_k;  // 1,000
constexpr auto b = 1_M;  // 1,000,000
constexpr auto c = 1_G;  // 1,000,000,000

// binary equivalents
constexpr auto d = 1_ki; // 1,024
constexpr auto e = 1_Mi; // 1,048,567
constexpr auto f = 1_Gi; // 1,073,741,824

Integral and enum values

_c is a variable template whose value is a std::integral_constant with inferred type.

constexpr auto i = stdx::_c<1>;        // std::integral_constant<int, 1>

enum struct E { value = 1 };
constexpr auto e = stdx::_c<E::value>; // std::integral_constant<E, E::value>

_c is also a user-defined literal that can be used to make a std::integral_constant<std::uint32_t, N>.

using namespace stdx::literals;

constexpr auto i = 1_c; // std::integral_constant<std::uint32_t, 1>

utility.hpp

as_unsigned and as_signed

as_unsigned and as_signed are useful functions for converting integral types to their opposite-signed type. Under the hood, it’s a static_cast to the appropriate std::make_unsigned_t or std::make_signed_t.

auto x = 1729;                 // int
auto y = stdx::as_unsigned(x); // unsigned int

CX_VALUE

CX_VALUE is a macro that wraps its argument in a constexpr callable, which can be used as a non-type template argument. The compile-time value can be retrieved by calling the callable. This is useful for passing non-structural types as template arguments.

// A std::string_view value cannot be a template argument,
// so wrap it in CX_VALUE
constexpr auto ts_value = CX_VALUE(std::string_view{});
auto o = stdx::optional<std:string_view,
                        stdx::tombstone_value<ts_value>>;
Note
This is supported for C++20 and later: it still requires the ability to pass lambda expressions as non-type template arguments. The type must still be a literal type to be used at compile time, but need not necessarily be a structural type. (The usual difference is that structural types cannot have private members.)

forward_like

forward_like is an implementation of std::forward_like. forward_like_t is also provided.

static_assert(stdx::same_as<stdx::forward_like_t<int &, float>, float &>);

FWD

FWD is a macro that perfectly forwards its argument. It’s useful for C++17 lambdas which can’t have a template head to name their types.

// C++20 and later possibility
auto l = [] <typename Arg> (auto&& arg) {
  return f(std::forward<Arg>(arg));
};

// equivalent
auto l = [] (auto&& arg) {
  return f(FWD(arg));
};

is_aligned_with

is_aligned_with is a function that returns true when a value "is aligned with" a type. That is, a pointer to that type would have a certain power-of-two alignment, and the value is a multiple of that.

static_assert(stdx::is_aligned_with<std::uint16_t>(0b1110));
static_assert(stdx::is_aligned_with<std::uint32_t>(0b1100));
static_assert(stdx::is_aligned_with<std::uint64_t>(0b1000));

is_aligned_with can also be used with pointer values.

std::uint32_t i;
assert(stdx::is_aligned_with<std::uint32_t>(&i));

overload

overload is a struct designed to encapsulate an overload set. It inherits from all the lambda expressions used to construct it. As an example, it’s useful for visiting a variant.

auto f(std::variant<int, float, std::string> const& v) {
  return std::visit(
    stdx::overload{
      [] (std::string const& s) { return s; },
      [] (auto num) { return std::to_string(num); }
    }, v);
}

sized

sized offers an easy way to convert array extents between arrays of different integral types.

How many times have you written something like this?

// I have N bytes, and I need to compute the number of uint32_ts to store them
const auto dword_size = (N + sizeof(std::uint32_t) - 1) / sizeof(std::uint32_t);

Or perhaps more likely and less portably:

const auto dword_size = (N + 3) / 4;

sized allows this conversion to be safer, more expressive, and more portable.

// I have N bytes, and I need to compute the number of uint32_ts to store them
const auto dword_size = stdx::sized8{N}.in<std::uint32_t>();

// I have M std::uint16_ts, and I need to compute the number of uint64_ts to store them
const auto qword_size = stdx::sized16{M}.in<std::uint64_t>();

// generally, I have K things of type T, I need to compute the number of Us to store them
const auto sz = stdx::sized<T>{K}.template in<U>();
  • sized8 is an alias for sized<std::uint8_t>

  • sized16 is an alias for sized<std::uint16_t>

  • sized32 is an alias for sized<std::uint32_t>

  • sized64 is an alias for sized<std::uint64_t>

The sized conversion works either from larger type to smaller type, or the other way around. Or even where the source and destination types are the same.

type_map

type_map is a structure designed to allow compile-time lookups of types or values. The basic idea is having a "map" of key-value pairs. Each key-value is a type_pair, and all the type_pair​s form a map. Values in the map can be looked up using type_lookup_t.

// A, B, C, X, and Y are types - they don't have to be complete
using M = stdx::type_map<stdx::type_pair<A, X>, stdx::type_pair<B, Y>>;
using T = stdx::type_lookup_t<M, A>; // X
using U = stdx::type_lookup_t<M, B>; // Y
using Z = stdx::type_lookup_t<M, C>; // void, because C is not in the map

type_lookup_t takes an optional third argument to be returned as the default (void above). The most common use case is for type_map to holds types, but there are convenience aliases for dealing with compile-time values in each of the four possibilities:

  • type_lookup_t - for mapping from types to types

  • type_lookup_v - for mapping from types to values

  • value_lookup_t - for mapping from values to types

  • value_lookup_v - for mapping from values to values

And type_pair has corresponding aliases to make the appropriate type_map​s:

  • tt_pair - for type-type maps

  • tv_pair - for type-value maps

  • vt_pair - for value-type maps

  • vv_pair - for value-value maps

// a type-type map that uses type_lookup_t
using M1 = stdx::type_map<stdx::tt_pair<A, X>, stdx::tt_pair<B, Y>>;
using T1 = stdx::type_lookup_t<M1, A>; // X

// a type-value map that uses type_lookup_v
using M2 = stdx::type_map<stdx::tv_pair<A, 0>, stdx::tv_pair<B, 1>>;
constexpr auto v2 = stdx::type_lookup_v<M2, A>; // 0

// a value-type map that uses value_lookup_t
using M3 = stdx::type_map<stdx::vt_pair<0, X>, stdx::vt_pair<1, Y>>;
using T3 = stdx::value_lookup_t<M3, 0>; // X

// a value-value map that uses value_lookup_v
using M4 = stdx::type_map<stdx::vv_pair<0, 42>, stdx::vv_pair<1, 17>>;
constexpr auto v4 = stdx::value_lookup_v<M4, 0>; // 42

In the case of mapping to types, the *_lookup_t aliases have optional third type arguments which are defaults returned when lookup fails. In the case of mapping to values, the *_lookup_v aliases have optional third NTTP arguments in the same role.

unreachable

unreachable is an implementation of std::unreachable.

[[noreturn]] inline auto unreachable() -> void {
  // if this function is ever called, it's
  // undefined behaviour
}