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.
Synopsis
Everything is in the stdx
namespace. Where suitable, functionality is grouped
into headers whose names match the standard.
The following headers are available:
atomic.hpp
atomic.hpp
provides an implementation of
std::atomic
with a few
differences.
stdx::atomic
does not implement:
-
is_lock_free
oris_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
andlowest_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 astd::uint16_t
-
to pack 2
std::uint16_t
s into astd::uint32_t
-
to pack 2
std::uint32_t
s into astd::uint64_t
-
to pack 4
std::uint8_t
s into astd::uint32_t
-
to pack 4
std::uint16_t
s into astd::uint64_t
-
to pack 8
std::uint8_t
s into astd::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
andto_ullong
are not implemented — butto
andto_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
functional.hpp
contains
bind_front and
bind_back.
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
andnext
pointers equal tonullptr
-
node
prev
andnext
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 hasstd::dynamic_extent
) -
stdx::span
(unless it hasstdx::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:
-
to_address
(from C++20)
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.
|
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 than5
-
6
,7
,0
,1
are all greater than5
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.
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
andY
are tag types; declared only and not defined. -
make_indexed_tuple
takes a number of type functions (here justkey_for
) that define how to look up elements. -
get
is working not with astd::size_t
index or the actual type contained within the tuple, but with the tag type that will be found bykey_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
- likestd::apply
, but also a member function ontuple
-
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
- likefor_each
, but including a compile-time index -
filter
- for compile-time filtering -
for_each
- like the standard version, but over a tuple -
fold_left
andfold_right
- member functions ontuple
-
join
- a member function ontuple
, likefold_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
- likestd::tuple_cat
-
tuple_cons
- add an element to the front of a tuple -
tuple_push_back
- alias fortuple_snoc
-
tuple_push_front
- alias fortuple_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:
-
conditional_t
(implemented with fewer template instantiations than a typical standard implementation) -
is_constant_evaluated
(from C++20) -
is_function_v
(implemented with Walter Brown’s method) -
is_scoped_enum_v
(from C++23) -
remove_cvref_t
(from C++20) -
to_underlying
(from C++23) -
type_identity
(from C++20)
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 forsized<std::uint8_t>
-
sized16
is an alias forsized<std::uint16_t>
-
sized32
is an alias forsized<std::uint32_t>
-
sized64
is an alias forsized<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
}