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
-
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:
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
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)
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_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, and
removal from the middle.
// 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>);
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 functonality
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.
get
is also available as a member function on stdx::tuple
and works either
by index or by type (using the tag
variable template):
using namespace stdx::literals;
auto t = stdx::tuple{1, true};
auto x_by_index1 = t.get(index<0>);
auto x_by_index2 = t.get(0_idx);
auto x_by_type = t.get(tag<int>);
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 is only defined for non-empty tuples.
auto t = stdx::tuple{1, 2, 3};
auto sum = t.join(std::plus{}); // 6
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
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
-
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
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);
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
.
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); // {true, 1}
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}
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
.
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");
}
};
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_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.
|
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.
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 passed to each of these functions.
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
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.
|
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
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));
};
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
}