Template Metaprogramming

Why you must get it

This is not about...

  • ... Functional programming
  • ... -ftemplate-depth=∞
  • ... Latest metaprogramming tricks (Really?)

But...

  • Real (Manu's) world templates
  • Okay, I confess, a bit of TMP madness
  • What's (should be) next?

$ whoami

  • Manu343726 @{twitter, reddit, github,...}
  • Usually ranting about C++ on twitter. Being --pedantic as a way of life
  • Full-time C++ for 4 years. Social life is overrated!
  • No OOP, but my own metaprogramming library...

$ whoami

  • biicode pulled me out of my cave! ... until I had to start working on Boost support...
  • cmake kung-fu
  • Leaving the cave: Blog posts on tmp. Exposure to sunlight (and C++ community)
  • C++ courses in my university
  • Now in By Tech working with embedded devices. Doing templates of course!

$ whoami

  • I still don't know how to write slides

Templates and Metaprogramming

Templates

  • Smart typed copy-paste system
  • Let the compiler do the boring work for you
  • Everything in a type-safe way

Metaprogramming

  • Programs that generate programs. WHAT?
  • const char[] stuff = { ... };( (void(*)())stuff)();  ? No, sorry
  • Feel like a hacker

Template metaprogramming

  • Hack the C++ type system to do useful work while compiling

"Programmer"

Organism that transforms coffee and pizza into lines of code

"C++ meta-programmer"

Mentally-diseased organism that transforms lines of code into time to go for a coffee

"C++ meta-programmer"

cmake --build .

(Don't) fear the templates

Use case 1: Array Views

Some context...

  • TCP/IP-like raw communication protocol

Some context...

  • Input: Receive frame, consume, discard
  • Output: Build frame, send, discard

Goals

  • Efficient stack processing
  • Clean usage

Array View

class ArrayView
{
    ArrayView(QByteArray& array);
    ArrayView(const ArrayView& source, 
              std::size_t begin, std::size_t end);
    ~ArrayView() = default; // Does nothing

    Byte operator[](std::size_t i) const;
    Byte& operator[](std::size_t i);
    ArrayView operator()(std::size_t begin, 
                         std::size_t end) const;

    template<typename T>
    T read() const; // Magic goes here
    template<typename T>
    void write(const T& value); // More magic
private:
    QByteArray* _array;
    std::size_t _begin, _end;
};

Non-owning view of a slice of an existing array

ArrayView IO

  • Serialization through read() and write() methods
  • Can be implicit, add conversion and assignment operators

ArrayView IO

class ArrayView
{
public:
    template<typename T>
    T read() const {
        QByteArray slice = _array->slice(_begin, _end);
        QDataStream stream{_&slice};

        T result;
        stream >> result;
        return result;
    }
};

Transparent serialization by calling read()/write()

ArrayView IO

  • Not that simple of course. Endianess, slicing, etc
  • Note that QByteArray does COW implicitly!

ArrayView recursive slicing

  • operator(): Take a slice out of the slice!
QByteArray array = readBytes();
ArrayView frame{array};

assert_equals(frame(0,2).read<quint16>(),
              FRAME_HEADER);

ArrayView payload = frame(4, 32);

appLayer(payload);

ArrayView recursive slicing

class ArrayView
{
public:
    ArrayView(const ArrayView& source, 
              std::size_t begin, std::size_t end) 
    : _array{source._array},
      _begin{source._begin + begin},
      _end{source._begin + end}
    {}

    ArrayView operator()(std::size_t begin, std::size_t end) const
    {
        return {*this, begin, end};
    }
};

ArrayView: Bonus

  • Wrap read()/write() with a proxy class
class ArrayView {
    ...
public:
    template<typename T>
    class ReaderWriter {
        const T& operator=(const T& value) {
            _view->write(value);
            return value;
        }

        ArrayView* _view;
    };

    template<typename T>
    ReaderWriter<T> readerWriter()
    {
        return {this};
    }
};

ArrayView: Bonus

  • Write a vieweable array class
  • Don't forget to take care of array reasignment on the view!
class ViewableArray : public QByteArray, public ArrayView {
public:
    ViewableArray(QByteArray& array);
    ...
};

ArrayView: Bonus

  • Write a Frame class following your specs
class Frame : public ViewableArray {
public:
    using ViewableArray::ViewableArray;

    ReaderWriter<quint16> header()
    ReaderWriter<quin32> source_address();
    ...
};

// Input

Frame frame = readBytes();

assert_equals(frame.header() == FRAME_HEADER);

// Output

Frame frame = buildRequest();

frame.source_address() = Me();

sendBytes(frame); Note sendBytes takes a QByteArray!

ArrayView

  • Don't be afraid of templates! Use them to write zero-overhead syntax suggar
  • More bonus: Each request frame is represented in the usual OO way as a class, but fields are ReaderWriters. OO like code, but no frame allocation/joining overhead.
  • Come to By Tech and see!

Use case 2:

Named Tuples

Named Tuples

  • I don't like std::get<>() at all.
  • C structs are fixed, but we can programatically manipulate tuples with metaprogramming

Goals

  • Human readable tuples please
  • Customizable tuple layout
  • Element access independent from layout

UX


auto tuple = make_tuple("pi"_, 3.141592654,
                        "e"_, 2.71828182846);

std::cout << "Pi: " << tuple["pi"_];

Use strings to name tuple elements

How?

  • I like to abuse operator overloading a lot
  • We have to map from strings to element index

constexpr hasing!

// Jonathan Müller, the constexpr hashing guy
namespace foonathan {
namespace string_id {
namespace detail {

using hash_type = std::uint64_t;
constexpr hash_type fnv_basis = 14695981039346656037ull;
constexpr hash_type fnv_prime = 109951162821ull;

// FNV-1a 64 bit hash
constexpr hash_type sid_hash(const char *str, hash_type hash = fnv_basis) noexcept
{
    return *str ? sid_hash(str + 1, (hash ^ *str) * fnv_prime) : hash;
}

}
}
} // foonathan::string_id::detail

Just write a constexpr UDL that computes the hash of the literal

constexpr hasing!

template<const char* str>
using hash = std::integral_constant<
    foonathan::string_id::detail::hash_type,
    foonathan::string_id::detail::sid_hash(str)
>;

constepr auto operator"" _(const char* str, std::size_t length)
{
    return hash<str>{};
}

Just write a constexpr UDL that computes the hash of the literal

Constexpr hashing

  • That doesn't work, sorry
  • Looking forward for variadic UDLs...
  • Let's follow with macros...

Constexpr hashing


#define h(x) std::integral_constant< \
    foonathan::string_id::detail::hash_type, \
    foonathan::string_id::detail::sid_hash(x) \
>{}

int main()
{
	auto t = make_tuple(h("pi"), 3.141592654, 
                            h("e"), 2.71828182846);
    
    std::cout << TUPLE_GET("e")(t) << std::endl;
    std::cout << TUPLE_GET("pi")(t) << std::endl;
}

Mapping

  • After hashing the strings, we have to map from hashes to tuple element indices
  • Let's write a compile-time hashmap

Mapping

// Not mine, Louis Dionne deserves all merit
namespace detail {
template <typename x>
struct no_decay { using type = x; };

template <typename key, typename value>
struct pair { };

template <typename ...xs>
struct inherit : xs... { };

template <typename key, typename value>
static no_decay<value> lookup(pair<key, value>*);

template <typename key, typename ...pairs>
using at_key = typename 
    decltype(lookup<key>((inherit<pairs...>*)nullptr))
::type;
}

Use function lookup rules to map from input types to "return" types

Let's Tuple

  • Our tuple will have both the elements and the keys, where keys are mapped onto the final element index

Let's Tuple

template<typename Indices, typename Types>
struct tuple;

template<typename... Indices, typename... Types>
struct tuple<std::tuple<Indices...>, std::tuple<Types...>> : 
    public std::tuple<Types...> 
{
    using tuple_t = std::tuple<Types...>;
    template<typename key>
    using element_index = detail::at_key<key, Indices...>;

    template<typename Hash>
    const auto& get() const {
        return std::get<
            element_index<Hash>::value
        >(static_cast<const tuple_t&>(*this));
    }
    template<typename Hash>
    const auto& operator[](Hash) const {
        return get<Hash>();   
    }

    template<typename... Ts>
    tuple(Ts&&... elems) : 
        tuple_t{ std::forward<Ts>(elems)... } {}
};

Take types and indices, map access to std::tuple

Tuple Bonus

  • I used std::tuple as backend just for convenience
  • Customize mapping policy: Customizable tuple layout independent from member access

Tuple Bonus

  • make_tuple() sugar: Take pairs of (hash, type)

Tuple Bonus

template<typename Hash, typename Type>
struct entry
{
	using hash = Hash;
	using type = Type;
};

template<typename... Entries>
struct tuple_builder
{
    template<typename Seq>   
    struct build;

    template<typename T, T... Seq>
    struct build<std::integer_sequence<T, Seq...>>
    {
  	using type = tuple<
            std::tuple<detail::pair<
                typename Entries::hash, 
                std::integral_constant<T,Seq>>...
            >,     
            std::tuple<typename Entries::type...>
        >;
    };

	using type = typename build<std::index_sequence_for<Entries...>>::type;
};

template<typename... Entries>
using build_tuple = typename tuple_builder<Entries...>::type;

Tuple bonus

  • Just check my github gists

C++ Metaprogramming

What's next?

The perfect metaprogramming library

X Metaprogramming Library

  • We need TMP libraries
  • Standarizing boolean traits is not enough
  • Support for "old" compilers
  • For C++98/03, use Boost.MPL

xml

  • C++11 only backend. Metafunction based
  • When that works, write all the C++14/17 suggar you want on top of it

Thank you!

TMP: Why you must get it

By Manu Sánchez

TMP: Why you must get it

My meetingcpp 2015 talk. Real use cases of tmp and my thoughts on a real-world tmp library. A great example of how you shouldn't title your talks.

  • 967
Loading comments...

More from Manu Sánchez