gurpreetshanky / dr_strangetemplate

A guide for the rest of us on using C++ templates.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Dr. Strangetemplate

Or How I Learned to Stop Worrying and Love C++ Templates

Templates! Meta-programming! <, >, and typename! The mere sight of these words/glyphs are enough to strike fear in the hearts of the meek and ignite righteous anger in the bosoms of well-intentioned code reviewers, who decry their use as too mysterious and arcane for good, plain production code. They may grudgingly admit that templates and metaprogramming have their use in library code (as with the venerable std::vector and other containers) but surely are a code smell anywhere else.

But no more! We live in a more enlightened age, and it's time to recognize the noble and simple truth of post-C++11 templates: they help you write better code. Templated code is type-safe, expressive, and can increase runtime performance by both shifting computation to compile-time and enabling optimizations that can't be applied to equivalent runtime code.

With modern C++ features, template code is readable, maintainable, sustainable1, biodegradable2, and fully embraceable3! A well-rounded C++ programmer should be able to identify when to use this powerful language feature.

What follows is a guide on writing practical, maintainable C++ template code. It is divided into case-studies of (more-or-less) real code that I have seen running freely in the wild, waiting to be boldly elevated into template modernity. So let's begin:

Table of Contents

Case Study 1: Consuming a C API

Imagine that I have a library with several functions as such:

Each function returns an int error code -- 0 represents no error, and other integers indicate some library-specific error. You might consume this API, observing proper error handling and logging as follows:

And so on, and so on. You'll write tons of code like this. Sometimes you won't be able to do anything meaningful with an error, so you just log it and move on. Maybe in some cases you'll want to change the behavior on success -- for instance if a call to major succeeds then you want to handle it in another thread. And then a few weeks later your supervisor decides you should do this with dmitri as well. And then that all API calls should be handled asynchronously on success, but reconsiders a few weeks after you've added hundreds of calls and decides that only API functions starting with the letter b should be handled synchronously.

So you find yourself sweating laboriously over your keyboard, doing tedious and undignified copy-and-paste, search-and-replace, and testing each change over and over again. Dear beleaguered programmer... there is a better way!

Everything just described can be achieved with templates! Easy refactoring, easily changeable success/error behavior, and the ability to select totally different behavior by using a different template (perhaps something like api_async_exec). By writing one template function we create a new api_exec for every API function that we have. We'll start by implementing a basic api_exec template function and gradually add more bells and whistles to it.

That's it! Now you're generating code like a pro. Note two things here:

  1. api_exec is a variadic template.
  2. The first parameter of api_exec is some weird function type.

A variadic template is a template that takes a variable number of template parameters4. If you've used templates before you may know that a template parameter is a type5 like int or MyCoolStruct. So a variadic template just takes some variable number of types that you don't have to specify. A variadic template's parameter pack can be expanded with Args... and used as a function parameter with Args... args. In this case Args... corresponds to the types of the parameters and args correponds to the actual values that we passed in.

Regarding the second point, the first rule of weird function types is that you shouldn't use a function type at all if you don't have to:

Whoa. Your compiler can deduce the type of func automatically when you use it as a parameter. Let it! It's what compilers love to do.

Abandon all hope, ye who enter here!

Variadic templates are a feature introduced in C++11 and they're really powerful, but they also introduce complexity. So do the rest of the features considered below, because as it turns out C++ templates define a whole 'nother programming language, one that's executed entirely at compile time and deals with types6.

You can get a lot of mileage out of basic templates like above. But if you understand metaprogramming techniques you can make good use of the standard library7, libraries like boost::hana, and write your own metafunctions for great profit. So read on if you wish to continue the brave march into template modernity!

Back to your regular program(ming)

There is one caveat to our first example -- because built-in numeric types are implicitly convertible from one to another, the compiler will quietly do stuff like this:

This isn't always undesirable behavior -- but since our C API always returns int anyway we may as well nip some weird mistake in the bud by creating a compiler error when you try to do silly stuff like above:

static_assert will generate a compiler error if its value is false. It doesn't do anything at all at runtime, so you should basically use it like it's going out of style to keep your code type-safe and readable.

More interesting is the expression std::is_integral<ReturnType>::value. std::is_integral is a metafunction that returns true if the type ReturnType is (you guessed it) integral8. This is our first example of metaprogramming!

Metafunctions take template parameters and the result is either another type or a constant value. In the case of is_integral we're interested in the bool value it returns, which by the standard library's convention is accessed in the static class variable value:

Now consider the previous line:

typedef is the equivalent of assigning a variable in metaprogramming, and ReturnType is the type name we're assigning it to. std::result_of is a metafunction that returns the type of the result of Function if it was applied to Args...9. Just like a metafunction's value can be accessed with ::value, by convention if it's the type we're interested in we access it through ::type as in std::result_of<Function(Args...)>::type. Finally we have to let the compiler know that an expression is a type and not a value, which you do with the keyword typename -- it's an unrelated double use of the keyword that appears in template parameter lists10.

Whenever you use a template inside of another template, you generally have to help the compiler deduce that the template is in fact a type by prefixing it with typename. So basically if you don't call it with ::value then you should use typename.

My mother said SFINAE is not a polite word

Finally let's write something that takes success and error callbacks:

Simple! We just add two more template parameters representing our success and error functions. But a perceptive reader might wonder what happens if you try to call this with an on_error function that doesn't take a single int parameter. Turns out it's a compile error.

Wait, weren't we promised an on_success callback that would automatically take the foo parameter we passed in? Let's write an overloaded function to handle that!

Ahh! This doesn't work. If you try to use it, then you'll get errors because the compiler has no way of knowing which overloaded function to pick. It can't figure it out from the template parameters, because variadic parameters "eat" up all the rest. In other words a parameter list like template <typename One, typename... TheRest> seems exactly the same as template <typename... SameAsTheLastOne>. If only there was some way to specify the metatype of the types in template parameters, just like you declare the types of variables in regular functions... And there is! But sadly in C++11 it's a bit clunky as you may infer from its weird acronym-name (acroname?) SFINAE.

SFINAE stands for "substitution failure is not an error" and refers to the rules of how C++ selects overloaded templates. Basically, in some circumstances if substituting a type would result in an error otherwise, the compiler will quietly ignore the error and try to select another template for overload resolution instead. SFINAE does not apply in function bodies -- we already saw this if you try to pass in an on error function that doesn't take a single int parameter. However it does apply to the return type of a template function.

You don't need to understand the details of SFINAE to start using it11. The standard library provides a metafunction called std::enable_if which takes one bool template parameter and one optional template parameter. When its first parameter is false, it simply results in a compiler error! You can use it as the return type of a function along with the metafunctions in type_traits to create overloaded templates that have constraints on their template parameters:

Let's break it down. First we define a new metafunction returns_void from the type_traits metafunctions for readability. It takes a single template parameter, and has a value member that's true if result_of applied to its argument is void. Next we replace the return type with std::enable_if:

The ::type of enable_if is void with the single-parameter version12, so the signature of api_exec hasn't changed. However if the predicate returns_void is false then this function will be removed from overload resolution because of SFINAE. We can define as many overloaded version as we want now!

This one will only be available to overload resolution if OnSuccess called with void returns true.

Huzzah! Let's use it:

Case closed

Example code for this case study is provided in case_study_1.hpp and case_study_1.cpp. Any typos or inaccuracies are my fault -- I would appreciate a PR!

A guide on metaprogramming would be remiss without mentioning C++ concepts, which have been proposed to greatly simplify selecting template overloads instead of using SFINAE. Concepts are currently availabe in GCC.

You can use the fundamental techniques presented to start writing great metaprograms, but if you get deep into it you'll probably want to use a library like the older MPL or the newer boost::hana.

Some readers have taken umbrage with this example -- see my thoughts in the issues.

Case Study 2: Building an awesome event interface

Templates can be used to create really great interfaces! They allow you to manipulate types in ways that wouldn't otherwise be possible. Consider the following pattern that I'll call Do Something When X Happens. It's a very simple pattern: whenever some particular event occurs, then one or more listeners respond to that event! An event occurring is realized as instantiating a class and providing it to a dispatcher. Listeners are recognized by providing them to the dispatcher and defining an appropriate handler member function. We'll start with an interface that we13 want and work backwards to build it:

The Do Something When X Happens pattern involves three actors -- an event class (here JustBeforeReturn) which is instantiated and provided to a Dispatcher. The Dispatcher in turn provides the event to a list of types which handle it. In this case the list of types is really just one, CoutShouter. Turns out this example will involve some nontrivial metaprogramming! Let's start with:

type_list: A metaclass (if you will)

Algorithms require data structures. The C++ standard library doesn't have data structures for types14, so in order to do anything other than trivial operations we'll have to define them. And it's actually really simple, although perhaps a bit unfamiliar compared to runtime C++. Template metaprogramming is a pure functional language15, so data structures look a little different than their runtime counterparts since runtime C++ is neither pure nor functional. The upshot is that we have plenty of rich examples to draw from. For instance, a type list could be defined as follows:

Since C++11 the using keyword can be used as a more natural typedef. Using metaclasses16 effectively requires good use of SFINAE, so before we go further I'm gonna let you in on a little trick17 to make using SFINAE much less awkward:

The metafunction void_t just maps any number of type parameters into void. It seems unremarkable at first until you realize that it can be used to invoke SFINAE since void_t's parameters must be well-formed! Here's a type_list metafunction that makes use of it:

This is a really great metaprogramming technique to have up your sleeves! The first template is the default case. It has an unused (and thus unnamed) default template parameter -- meaning importantly that you can call it by passing in one template parameter only, the type that you're interested in. The metafunction count will return 1 by default, but if the type passed in has a ::tail like our type_list, then it will peel it off and recursively call count until it hits the default case, adding one to its value each time.

The template specialization below it is where the magic happens. void_t<typename T::tail> will invoke SFINAE if the template parameter T does not have a tail member! And since it is more specialized18 the compiler will always prefer it unless SFINAE removes it from overload resolution. Inheriting from std::integral_constant allows a type to be used in a numeric context, which we'll find very useful shortly.

The big takeaway here is that void_t can be used to really easily determine if a type has some particular member. Along with enable_if (which can also be used for this purpose, but the implementation is much more verbose) we can start building much more complex data structures and metafunctions.

Some readers have pointed out that count can be implemented with fewer template instantiations. And they're right! So check out this alternate implementation that doesn't use SFINAE at all:

We only define a specialization here -- you can't instantiate different_count with anything other than a type_list. This is an example of pattern matching, which we'll see used to good effect in the next example! The interesting thing to note here is that matching the pattern type_list<Elts...> actually unpacks Elts so that we can use it elsewhere in the template, namely as the argument of sizeof..., which counts the number of types in a parameter pack.

Here's another metafunction that we'll be using:

has_tail uses std::conditional to inherit from either false_type or true_type depending on what the predicate evaluates to. It's the functional equivalent of the ternary operator, choosing the first type if its predicate is true, otherwise the second type. false_type and true_type are specializations of our friend integral_constant that allow a class to be used in a boolean context19.

Dispatcher: The Dispatchening

We've got nearly everything we need to write dispatcher. It looks like this:

The has_handler metafunction determines if the parameter Handler has a static member function handle that takes a const Evt& parameter. Note again the default template parameter in the primary definition -- that's a sign that we're about to make use of SFINAE. And indeed, the specialization below it reveals one more SFINAE technique to add to our collection. Since C++11 the keyword decltype can be used to determine the declared type of an expression without evaluating that expression. You can use it to determine if a type has a member function (handle here) that takes some arbitrary parameters (here const Evt&). If the expression inside of decltype is well-formed then the result will be the function's return type. Otherwise SFINAE will be invoked!

std::declval is another standard library metafunction that we can use to instantiate types inside of declval. The expression decltype( Handler::handle( const Evt& ) ) will produce an error because we need to call handle with an instance of const Evt&. The expression std::declval<const Evt&>() gives us just that20.

Dispatcher::post defers its call to another template, post_impl which takes four parameters. Two of the parameters (HasTail and HasHandler) are completely determined by the Listeners parameter. The strategy for defining post_impl will be to write four template specializations that do different things depending on the values of HasTail and HasHandler, which tell us if the type_list has a tail we need to peel off and whether type_list::head has an appropriate handler for Evt, respectively.

Note that post_impl is actually a type and we're really calling its static member function call. That's because we rely on partial template specialization, which is only allowed with template classes and not template functions. Wrapping such functions in a class is a work-around. Here is the specialization for when both conditions are true:

If the ::head of the list has an appropriate handler, then we call it! If the list has a ::tail, then we peel it off and call post_impl on the tail. We pass in conditions that allow the appropriate specializations to be chosen depending on whether the next element in the list has a handler and whether it has a ::tail or not. And that's it21!

Wrapping up the Dispatcher example

Here's some code that illustrates use of the Do Something When X Happens pattern:

With template code like this it's reasonable to expect that the compiler could inline all of the Dispatcher code.22

Complete code examples can be found in case_study_2.hpp and case_study_2.cpp.

Case study 3: A compile-time container metaclass

The final case study for now is meant to give a taste of more advanced metaprogramming. The ad-hoc type_list data structure structure above is fine and dandy, but can we create an honest-to-goodness std::vector-style random access container for types?

Turns out the answer is not only "Heck yes!" but that lots of library writers have already done it! So go use those in production code instead of writing your own. But if you want to learn a little bit about how those implementations might work, read on. We'll use some features from the C++14 std:: namespace, like integer_sequence -- but they're features that could have been implemented in C++11.

First we must note that with purely functional data structures the naive implementation of a random-access list has really bad (linear) performance for accessing an arbitrary element. This might seem surprising since C-style arrays trivially have constant-time access to arbitrary elements, so why shouldn't type lists? Indeed for a fixed-length type list the obvious implementation is to reflect the arguments back for constant-time element access:

In order to do this with an arbitrary number of type parameters, we'll make each element of the list a distinct base class, and implement an accessor function that casts the tuple to the appropriate class23. This is possible due to the rules of template argument deduction, which we'll examine in more detail later.

First we define an element. Its Type is the payload, and the unused Index parameter will turn out to be critical to the template argument deduction for the accessor function. The forward declaration of tuple_impl will match a std::index_sequence and its variadic parameter will become a pack of elements.

This specialization is the heart of tuple_impl. Note that the first parameter of the specialization is not the parameter pack Ns, but rather it's the std::index_sequence<Ns...>. This demonstrates the purpose of std::index_sequence -- when you instantiate a template with an index_sequence then it will match a specialization like above. Then through template argument deduction the values of the sequence will be unpacked into the parameter Ns. If you're familiar with other functional languages, this is a C++ template metaprogramming technique for pattern matching!

Note also that tuple_impl inherits from some number of element specializations. The pack expansion will work on Ns and Types simultaneously to produce a sequence like element<0, foo>, element<1, bar>, etc.

Now we actually define tuple, which just takes a variadic parameter Types and constructs a tuple_impl from it. For instance tuple<int, int, char*> will have the base classes tuple_impl<std::index_sequence<0, 1, 2>, int, int, char*>, element<0, int>, element<1, int>, and element<2, char*>.

That's it! But if we try to access the ::type of a tuple it's ambiguous since more than one base class defines that type alias. We have to cast a tuple to one of its base classes in order to unambiguously access it. If we know exactly what one of the base classes is we could static_cast, e.g. static_cast<element<1, int>>(my_tuple_instance). But that defeats the purpose because we don't know what the second template parameter of element will be. Here's where we'll rely on template argument deduction. We'll declare a function that takes the base class we want to select as a parameter and returns its ::type:

If we instantiate this template for example with get<1>(tuple<int, int, char*>()) then we're telling the compiler to conisder calling get<1, T>(const element<1, T> &). Since the argument is a class that is derived from element<1, T>, then it can deduce unambiguously what T is -- in this case int. And we don't actually have to supply a definition of get, because we're just using it in an unevaluated context for the template argument deduction:

The template at finds the return type of get if we supplied it with the parameters N and Tuple. Here's at in action:

Example code can be found in case_study_3.hpp and case_study_3.cpp.

Further reading

Metaprogramming is about computing with types in ways that aren't possible with runtime C++.

If you're wondering why computing with types is even necessary, outside of the performance benefits that metaprogramming can enable, then here's a presentation on using types effectively in C++.

If you're thinking about metaprogramming in production code, or if you just want some great examples of metaprogramming in action, take a look at the boost::hana user manual.

Why did you write this guide?

I wanted to learn about C++ template metaprogramming, but many of the resources I found were either:

  • In reference to pre-C++11, which introduces a lot of new features for metaprogramming.
  • Too simple. (Not another factorial example!)
  • Too advanced.

My aim is for this guide to fill a gap between "too simple" and "too advanced" by showing short but non-trivial applications of metaprogramming.

Who are you?

Michael Gallaspy, variously a professional software engineer, substitute teacher, Peace Corps volunteer, whitewater raft guide, nature appreciater, enthusiastic exister, and enjoyer of Dr. Strangelove.

Resumes available upon request, and if you're reading this and you're my current employer consider giving me a raise. ;)


  1. In a manner of speaking.

  2. Actually not.

  3. But this part is true.

  4. Kinda like regular variadic functions.

  5. Actually a template parameter can also be an integral type, e.g. template <int N>, another template, and some other stuff too. Check it out!

  6. It also turns out you can make a trade-off by turning some runtime computations into compile-time computations, although since C++11 it's much easier to do this with constexpr than with template metaprogramming.

  7. The standard library provides metafunctions in the type_traits header, and support only gets better in C++14, C++17, and undoubtedly future versions as well.

  8. Like int or const int.

  9. If Function is not actually a function then gcc will raise an error with C++11.

  10. Like template <typename Unrelated>.

  11. Although it wouldn't hurt.

  12. The two-parameter version returns its second parameter as its ::type, e.g. std::enable_if<true, int>::type is int.

  13. And by "we" of course I mean "I".

  14. Although libraries like boost::hana do!

  15. It really is pure in the sense that it has both no side-effects and its results are totally determined by the inputs, and not affected by user's runtime decisions, the weather, and other random occurrences. Other functional languages include Haskell and Scala.

  16. The term metaclass doesn't seem to be in common use among C++ template metaprogrammers. Which is a shame because it sounds cool. I use it here in an imprecise sense to mean template classes.

  17. Actually the trick is well-known and is in the standard library since C++17. See this great talk by Walter E. Brown.

  18. C++ will pick the most specialized template that matches an invocation.

  19. This is an inefficient implementation. Better would be to use SFINAE to check for a ::tail member directly. Here I use count only for compactness -- it becomes a one-liner!

  20. Strangely enough declval is undefined. Only its declaration exists. You can't use it to actually instantiate anything, it can only be used in unevaluated contexts. I know of no use for it outside of decltype expressions.

  21. The other specializations fall down similarly.

  22. This will be implementation dependent. But when I make assembly and inspect the generated assembly of <main> for case study 2, I only see one function call to a name-mangled ComicBookNerd::handle(ReadingComicBooks const&)! Pretty compelling evidence that the rest was inlined. For me this is totally dependent on optimization levels --without -O3 enabled it's clear from the assembly that nothing is inlined.

  23. I got this clever idea from the author of boost::hana!

About

A guide for the rest of us on using C++ templates.

License:MIT License


Languages

Language:C++ 94.2%Language:Makefile 5.8%