This is my learning notes from the course - Mastering C++ Standard Library Features.
Creating a Compile-Time Set Data Structure
- How to implement a simple compile-time set data structure
The Plan
- Our data structure will be called
ct_set
- It will allow users to add/remove/check elements
- Every element will have a unique type
- It will use a familiar
constexpr
- based syntax:1
2
3
4
5constexpr auto s0 = ct_set{};
constexpr auto s1 = s0.add(c<42>);
static_assert(s1.contains(c<42>));
static_assert(!s1.contains(c<120>));
Implementation
We will need following headers:
1
2
3Using C++17’s “
auto
non-type template parameters” andinline
variables, we can easily define convenient shorthand notation forstd::integral_constant
1
2
3template <auto X>
inline constexpr std::integral_constant<decltype(X), X> c{};
// c<10> -> std::integral_constant<decltype(10), 10>Our
ct_set
data structure will store items thanks to a variadic parameter packItems...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73template <typename... Items>
struct ct_set
{
// Since `this` is not usable as part of a constant expression
// we'll define a `static` `self()` member function
// that return a new instance of the current set
static constexpr auto self() noexcept
{
return ct_set<Items...>{};
}
// In order to check whether or not an item is in the set, we'll define a
// `contains` function that takes an arbitrary item of type `T` and uses
// `std::disjunction` to check if any of the stored `Items...` match `T`
template <typename T>
constexpr bool contains(T) const noexcept
{
return std::disjunction_v<std::is_same<T, Items>...>;
}
// Adding an item `x` to the set will return a new instance of:
// * The same set, if the item is already available
// * A new set of `T, Items...`, if the item is not available
template <typename T>
constexpr auto add(T x) const noexcept
{
if constexpr(self().contains(x))
{
return self();
}
else
{
return ct_set<T, Items...>{};
}
}
// Removing an item `x` from the set will return a new instance of:
// * The same set, if the item is not available
// * A new set of `Items... - {T}`, if the item is already available
template <typename T>
constexpr auto remove(T x) const noexcept
{
if constexpr(!self().contains(x))
{
return self();
}
else
{
// clang-format off
// A simple way of removing a particular type from a type list
// is by using `std::tuple` and `std::tuple_cat`.
// Firstly, we'll wrap every item that is not `T` in an `std::tuple`.
// The item that matches `T` will be transformed to an empty tuple.
// Then `std::tuple_cat` will concatenate and flatten the tuples.
constexpr auto filtered = std::tuple_cat(
std::conditional_t<
std::is_same_v<T, Items>,
std::tuple<>,
std::tuple<Items>
>{}...
);
// Now that we have a tuple containing all types except `T`
// we can use `std::apply` in order to extract those types
// and get back a new instance of `ct_set` without `T`
return std::apply([](auto... xs)
{
return ct_set<decltype(xs)...>{};
}, filtered);
// clang-format on
}
}
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16int main()
{
constexpr auto s0 = ct_set{};
static_assert(!s0.contains(c<42>));
constexpr auto s1 = s0.add(c<42>);
static_assert(s1.contains(c<42>));
constexpr auto s2 = s1.add(c<100>);
static_assert(s2.contains(c<42>));
static_assert(s2.contains(c<100>));
constexpr auto s3 = s2.remove(c<42>);
static_assert(!s3.contains(c<42>));
static_assert(s3.contains(c<100>));
}
Summary
- Using
<type_traits>
,<utilities>
, and<tuple>
, it is possible to easily implement compile-time data structures - Every element needs to have a unique type -
std::integral_constant
is helpful to achieve that - This is not usable in constant expressions - defining a static
self()
member function circumvents the issue - It is possible to achieve a familiar syntax by using
constexpr
member functions
Guidelines
- When necessary and possible, consider creating compile-time data structures for your domain problem
- Compile-time errors are better than run-time errors
- Possible performance improvements
std::tuple
is useful even when storing simple empty classes- Consider using production-ready libraries, such as
boost::hana
Section Summary
- Learned that metafunctions are functions that operate on types. They can be used to manipulate or adapt types in templates
- Studied that the Standard Library provides many metafunctions and metaprogramming utilities in the
<type_traits>
and<utilities>
headers - The
<type_traits>
header can be used to mutate types or check what properties/operations they satisfy - It is possible to create convenient compile-time data structures by combining
constexpr
and metaprogramming utilities from Standard Library