0%

Metaprogramming Utilities in the C++ Standard Library

This is my learning notes from the course - Mastering C++ Standard Library Features.

Metaprogramming Utilities in the Standard Library

  • <type_traits> header
    • std::conditional
    • std::integral_constant
    • std::conjunction and std::disjunction
  • <utilities> header
    • std::integer_sequence
    • std::make_integer_sequence

< type_traits > - Overview

Example: Using to Assert Type Properties

1
2
3
4
5
6
7
template <typename T>
void store(T&& x)
{
static_assert(!std::is_polymorphic_v<T>, "`T` must not be a polymorphic type");

// ...
}
  • Produces a clear error message and prevents mistakes

Example: Using to Constrain Overloads

1
2
3
4
5
6
7
8
9
10
11
template <typename T>
auto store(T&& x) -> std::enable_if_t<std::is_polymorphic_v<T>>
{
// ...
}

template <typename T>
auto store(T&& x) -> std::enable_if_t<!std::is_polymorphic_v<T>>
{
// ...
}

Example: Using with if constexpr

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T>
void store(T&& x)
{
if constexpr(std::is_polymorphic_v<T>)
{
// ...
}
else
{
// ...
}
}

Properties and Predicates

  • Useful to assert type properties and produce better error messages (that is using static_assert)
  • Useful to constrain overloads depending on type properties
    • In C++11/14, use std::enable_if
    • In C++17, consider using if constexpr(...)

std::conditional

  • std::conditional<Predicate,T,F> evaluates to:
    • T if Predicate == true
    • T if Predicate == false
  • This can be used to choose a type depending on a compile-time condition or type property

Example: std::conditional Basic Usage

1
2
3
4
template <typename T>
using container_for = std::conditional_t<
sizeof(T) <= 64, std::vector<T>, std::list<T>
>;
1
2
static_assert(std::is_same_v<container_for<int>, std::vector<int>>);
static_assert(std::is_same_v<container_for<huge_obj>, std::vector<huge>>);

Example: Wrapping a T& with std::reference_wrapper

1
2
3
4
5
template <typename T>
struct items
{
std::vector<T> _data;
};
1
2
items<int> i0;   // <-- OK
items<int&> i1; // <-- Compilation error
  • std::vector<T&> is invalid, as T& is not Assignable

1
2
3
4
5
6
7
8
9
template <template T>
struct items
{
using item_type = std::conditional_t<
std::is_reference_v<T>,
std::reference_wrapper<std::remove_reference_t<T>>,
T>;
std::vector<item_type> _data;
};
1
2
items<int> i0;   // <-- OK
items<int&> i1; // <-- OK
  • No need to specialize items in its entirety

std::disjunction and std::conjunction

  • Introduced in C++17, these utilities allow you to perform a logical OR/AND on a sequence of traits
    • std::disjunction<Ts...> -> logical OR
    • std::conjunction<Ts...> -> logical AND
  • These utilities are short-circuiting: if a result is known, the ::value of the rest of the passed traits will not be instantiated
  • In contrast, C++17 fold expressions do not short-circuit

Example: Any/All Trait Satisfaction

1
2
3
4
5
template <typename T, template <typename> typename ... Traits>
using satisfies_all = std::conjunction<Traits<T> ...>;

template <typename T, template <typename> typename ... Traits>
using satisfies_any = std::disjunction<Traits<T> ...>;
1
2
3
template <typename T>
void foo() -> std::enable_if_t<satisfies_all<T, std::is_pointer, std::is_const>{}>
{ /* ... */ }

std::integral_constant

  • std::integral_constant is defined as follows:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    template <typename T, T Value>
    struct integral_constant
    {
    static constexpr T value = Value;

    using value_type = T;
    using type = integral_constant;

    constexpr operator value_type() const noexcept;
    constexpr value_type operator()() const noexcept;
    };
  • std::integral_constant allows you to encode a value in a type:

    1
    2
    using five = std::integral_constant<int, 5>;
    static_assert(five{} == 5);
  • Five can be instantiated and passed around to templates as an empty value. The constant can be recovered thanks to the constexpr conversions:

    1
    2
    3
    4
    5
    template <typename T>
    void foo(T x)
    {
    static_assert(x == 5);
    }
    1
    2
    using five = std::integral_constant<int, 5>;
    foo(five{});
  • To recap,

    1
    2
    3
    4
    5
    void foo(int x)
    {
    static_assert(x == 10); // <-- Compile-time error
    }
    foo(10);
    1
    2
    3
    4
    5
    6
    template <typename T>
    void foo(T x)
    {
    static_assert(x == 10); // <-- OK
    }
    foo(std::integral_constant<int, 10>{});

  • std::integral_constant is an example of the type-value encoding idiom (a.k.a. “dependent typing”)
  • It blurs the line between types and values:
    • It encodes a numerical value as part of its type
    • It can be passed around as a value, allowing compile-time retrieval of the stored constant
  • Often used for tag dispatching
  • The Standard Library provides common aliases:
    • std::bool_constant<B> (since C++17)
    • std::true_type
    • std::false_type

Example: Tag Dispatching

1
2
3
4
5
6
7
8
template <typename T>
void store_impl(T&& x, std::true_type /* empty */) { }

template <typename T>
void store_impl(T&& x, std::false_type /* non-empty */)
{
this->storage_for<T>().add(std::forward<T>(x));
}
1
2
3
4
template <typename T> void store(T&& x)
{
store_impl(std::forward<T>(x), std::is_empty<T>{});
}

std::integer_sequence

  • std::integer_sequence<T, IS...> represents a compile-time sequence of integers

  • std::index_sequence is an alias for std::size_t integer sequences

    1
    2
    3
    using s0 - std::integer_sequence<int, 0, 5, 2>;
    using s0 - std::integer_sequence<int>;
    using s0 - std::index_sequence<5, 4, 3, 2, 1>;
  • It allows you to “package” an arbitrary amount of integers and “match” them in a template function call

    1
    2
    3
    4
    5
    6
    template <std::size_t... Is>
    void foo(std::index_sequence<Is...>)
    {
    bar(some_array[Is]...);
    }
    foo(std::index_sequence<0, 1, 2, 3>{});

  • Common use case: generating sequences from 0 to N-1
  • The Standard Library provides:
    • std::make_integer_sequence<T, N>
    • std::make_index_sequence<N>
      1
      2
      using s0 = std::make_index_sequence<5>;
      static_assert(std::is_same_v<s0, std::index_sequence<0, 1, 2, 3, 4>>);

Example: Invoking a Function with Elements of std::array

  • Firstly, let us define an apply_array function that creates an index_sequence and forwards everything to an implementation:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    template <typename F, typename Array>
    decltype(auto) apply_array(F&& f, Array&& a)
    {
    return apply_array_impl(
    std::forward<F>(f),
    std::forward<Array>(a),
    std::make_index_sequence<a.size()>{}
    );
    }
  • The implementation will “match” the index sequence (Is) indices and use pack expansion inside a function call to f:

    1
    2
    3
    4
    5
    template <typename F, typename Array, std::size_t... Is>
    decltype(auto) apply_array_impl(F&& f, Array&& a, std::index_sequence<Is...>)
    {
    return std::forward<F>(f)(a[Is]...);
    }
    1
    2
    3
    4
    std::array<int, 5> a{3, 6, 1, 2, 4};
    auto sum = apply_array([](auto... xs){ return (xs + ...); }, a);

    assert(sum == 3 + 6 + 1 + 2 + 4);

Example: Compile-Time Iteration Over Integers

  • Goal: Have a repeat(f) function that invokes f N times, passing the current index

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    repeat<16>([](auto i)
    {
    if constexpr(i % 2 == 0)
    {
    std::cout << i << "is even\n";
    }
    else
    {
    std::cout << i << "is odd\n";
    }
    });
  • Firstly, let’s create the interface repeat function:

    1
    2
    3
    4
    5
    template <std::size_t N, typename F>
    void repeat(F&& f)
    {
    repeat_impl(std::forward<F>(f), std::make_index_sequence<N>{});
    }
  • Let us “match” the index_sequence and use a fold expression to invoke f with integral_constant:

    1
    2
    3
    4
    5
    template <typename F, std::size_t... Is>
    void repeat_impl(F&& f, std::index_sequence<Is...>)
    {
    ( f(std::integral_constant<std::size_t, Is>{}), ... );
    }

Summary

  • <type_traits> provides
    • std::conditional
    • std::integral_constant
    • std::conjunction
    • std::disjunction
  • Utilities provides
    • std::integer_sequence
    • std::make_integer_sequence
  • std::conditional<B, T, F> can be used to select a type depending on a compile-time condition
  • std::conjunction and std::disjunction can be used to compose traits in a short-circuiting manner
  • std::integral_constant allows you to encode a value in a type, and then conveniently retrieve it later on
  • std::integer_sequence and std::make_integer_sequence allow you to create compile-time sequence of integers

Guidelines

  • Use std::conditional when you need to change a small part of a class without completely specializing it
  • Use std::conjunction and std::disjunction to compose type traits together
  • Consider using std::integral_constant as part of your interfaces to allow users to conveniently pass compile-time constants as values
  • Use std::integer_sequence to expands collections like array and tuple at compile-time