0%

Metafunctions in C++

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

Computations on Types

  • Manipulation of types and type lists
  • template metaprogramming
  • Metaprogramming utilities in the Standard Library
  • The “type-value encoding“ idiom

Metafunctions

  • The idea behind “type manipulation”
  • Metafunctions
  • Useful use cases

Type Manipulation

  • C++’s type system is way more powerful than it seems
    • Values can be encoded into types
    • Types can be encoded into values
  • By using templates and constexpr, functions that perform arbitrary computations on types can be implemented
    • More generic, safer code
    • Embedded domain-specific languages
    • “Expression templates”
  • Here’s a very simple type manipulation: Turning a type into a pointer
    1
    2
    3
    4
    5
    6
    template <typename T>
    class my_vector
    {
    T* _buffer;
    // ^~
    };
    • Given a type T, we are transforming it into T*
    • What if we need to perform the opposite transformation
      • given a T*, get T back
    • Templates allow us to implement functions that operate on types
      • These are commonly called “metafunctions

Metafunctions - Explicit Specialization

1
2
3
4
5
6
7
8
9
10
template <typename T>
struct remove_pointer;

template <typename T>
struct remove_pointer<T*> { using type = T; };

template <typename T>
using remove_pointer_t = typename remove_pointer<T>::type;

static_assert(std::is_same_v<remove_pointer_t<int*>, int>);
  • remove_pointer “matches” a T* and “returns” a T
  • A remove_pointer type alias is provided in order to reduce boilerplate
  • std::is_same_v<T, U>, available in <type_traits>, evaluates to true if T and U are the same type

Sneak Peek:

  • Since C++11, the Standard Library provides <type_traits>
  • This header contains metafunctions that either:
    • Manipulate types like:
      • std::remove_pointer, std::make_unsigned
    • Check properties of types
      • std::is_aggregate, std::is_copy_constructible
      • std::is_same, std::is_lvalue_reference

Metafunctions - decltype and std::declval

  • Another way of defining the same transformation is by declaring a function template and then using decltype to get it’s return type
    1
    2
    3
    4
    5
    template <typename T>
    auto remove_pointer(T*) -> T;

    template <typename T>
    using remove_pointer_t = decltype(remove_pointer(std::declval<T>()));

Example: Forwarding Object into Wrapper Class

1
2
3
4
5
6
7
8
9
10
11
template <typename Transport>
class server
{
Transport _transport;

public:
void send(int x) { _transport.send(x); }

template <typename T>
server(T&& transport) : _transport{std::forward<T>(transport)} { }
};
  • server<Transport> stores a Transport instance
  • Server’s constructor takes a forwarding reference and then initializes the _transport data member
    1
    2
    3
    4
    struct mock_udp { /* ... */ }

    mock_udp t;
    server<mock_udp> s{t};
  • To avoid repetition, let’s create a make_server helper function
1
2
3
4
5
template <typename T>
auto make_server(T&& transport)
{
return server<T>{transport};
}
1
2
mock_udp t;
auto s = make_server(t);
  • What’s the type of s?

    • It’s server<mock_udp&>, due to forwarding reference rules
    • Not what we want
  • If we want to make sure that transport is either copied or moved into server, we need to manipulate T

    1
    2
    3
    4
    5
    template <typename T>
    auto make_server(T&& transport)
    {
    return server<std::remove_reference_t<T>>{transport};
    }
    1
    2
    3
    4
    mock_udp t;
    auto s0 = make_server(t);
    auto s1 = make_server(mock_udp{});
    // `s0` and `s1` have type `server<mock_udp>`
  • In this example, using a metafunctions allowed us to:

    • Avoid code repetition thanks to perfect-forwarding and an helper make_server function
    • Ensure that no reference were accidentally stored in server

Applying Metafunctions

in Place

1
2
3
4
5
template <typename T>
auto make_server(T&& transport)
{
return server<std::remove_reference_t<T>>{transport};
}
  • In this example, we are applying remove_reference_t to T directly where it’s used
  • This is the recommended technique when dealing with one or two short metafunctions

Type Aliases

  • Type aliases are really helpful when applying several metafunctions in a row
    1
    2
    3
    4
    5
    6
    7
    template <typename T>
    struct complicated_metafunction
    {
    using step0 = std::make_signed_t<T>;
    using step1 = std::add_pointer_t<step0>;
    using type = std::add_cv_t<step1>;
    };

Variadic Templates

1
2
3
4
5
6
7
8
template <typename ... Ts>
using decay_tuple = std::tuple<std::decay_t<Ts> ...>;

static_assert(std::is_same_v
<
decay_tuple<int[], const int>,
std::tuple<int*, int>
>);
  • Variadic templates and type aliases help with composition of metafunctions for an arbitrary amount of types

Example: Converting C String Literals to std::string

1
2
3
4
5
6
7
8
9
10
11
template <typename T>
struct to_std_string { using type = T; };

template <>
struct to_std_string<const char*>
{
using type = std::string;
};

template <typename T>
using to_std_string_t = typename to_std_string<T>::type;
1
2
3
4
5
6
7
8
9
10
11
template <typename ... Ts>
auto make_my_tuple(Ts ... xs)
{
return std::tuple<to_std_string<Ts> ...>{std::move(xs) ...};
}

static_assert(std::is_same_v
<
decltype(make_my_tuple(10, "hello")),
std::tuple<int, std::string>
>);
  • This technique is useful when you expect to mutate the passed strings later on in the program and you want to make the interface as simple as possible for your user
  • It can be easily expanded to match const char(&)[N]:
    1
    2
    3
    4
    5
    template <std::size_t N>
    struct to_std_string<const char(&)[N]>
    {
    using type = std::string;
    };

Summary

  • Learned that metafunctions are functions that operate on types
    • They are implemented using templates and type aliases
  • Understood that the Standard Library provides many metafunctions in the <type_traits> header
    • They can be used to check properties of types or manipulate them

Guidelines

  • Provide a type alias for your metafunctions in order to simplify usage syntax
  • decltype, std::declval<T>(), and std::is_same_v can help you implement and test your metafunctions thanks to static_assert
  • Use std::remove_reference_t when implementing helper make functions for classes with a perfect-forwarding constructor
  • Use metafunctions in the context of type deduction to streamline your interfaces and prevent surprising results