Modern C++ Templates: Unleashing the Power (And Apologies for the Absence)

Enzo HugonnierEnzo Hugonnier
5 min read

What Are Templates, and Why Should I Care?

Let's start with the basics. Templates are the backbone of generic programming in C++. They let you write code that's flexible and reusable, regardless of the specific data types involved. It's like having a master key that unlocks a whole world of possibilities.

Think of a function to find the maximum of two values:

template <typename T>
T max_value(T a, T b) {
    return (a > b) ? a : b;
}

Boom! This single max_value function can find the maximum of integers, doubles, even custom objects – no need to write separate functions for each type. That's the power of templates.

Exploring the Template Universe

Function Templates

Function templates are the most common use of templates. They allow functions to operate with generic types, making them extremely versatile. Here's another example:

template <typename T>
T square(T value) {
    return value * value;
}

You can call this square function with an int, float, double, or even user-defined types, provided that the multiplication operator is defined for them.

Class Templates

Class templates are equally powerful. They allow you to create classes that can operate with any data type. For example, consider a simple pair class:

template <typename T1, typename T2>
class Pair {
public:
    Pair(T1 first, T2 second) : first_(first), second_(second) {}
    T1 getFirst() const { return first_; }
    T2 getSecond() const { return second_; }
private:
    T1 first_;
    T2 second_;
};

You can instantiate this class with different types:

Pair<int, double> myPair(1, 2.5);
std::cout << "First: " << myPair.getFirst() << ", Second: " << myPair.getSecond() << std::endl;

Variable Templates

Variable templates, introduced in C++14, allow you to define variables with templates. Here's a simple example:

template <typename T>
constexpr T pi = T(3.1415926535897932385);

You can use pi with different types:

double pi_double = pi<double>;
float pi_float = pi<float>;

Alias Templates

Alias templates provide a convenient way to create type aliases for template types. They are especially useful for simplifying complex template definitions:

template <typename T>
using Vec = std::vector<T>;

Instead of writing std::vector<int>, you can now write Vec<int>.

Modern C++ Template Tricks: The Fun Begins

C++11 and beyond have revolutionized templates with new features that'll make your head spin (in a good way):

Variadic Templates

Variadic templates allow you to write templates that accept any number of arguments. This is useful for creating flexible and reusable code. Here's a function that calculates the sum of an arbitrary list of numbers:

template <typename... Ts>
auto sum(Ts... args) {
    return (args + ...); // Fold expression magic!
}

You can call this sum function with any number of arguments:

std::cout << sum(1, 2, 3, 4, 5) << std::endl; // Outputs 15

Template Metaprogramming

Template metaprogramming involves using templates to perform computations at compile time. This can lead to highly efficient code by shifting computations from runtime to compile time. Here's an example of a compile-time factorial calculation:

template <int N>
struct Factorial {
    static constexpr int value = N * Factorial<N - 1>::value;
};

template <>
struct Factorial<0> {
    static constexpr int value = 1;
};

int main() {
    std::cout << "Factorial of 5: " << Factorial<5>::value << std::endl; // Outputs 120
    return 0;
}

Concepts (C++20)

Concepts are a way to specify constraints on template parameters, making your template code more expressive and less error-prone. Here's a simple example:

template <typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::same_as<T>;
};

template <Addable T>
T add(T a, T b) {
    return a + b;
}

Now, the add function can only be instantiated with types that satisfy the Addable concept.

Fold Expressions (C++17)

Fold expressions provide a concise way to apply an operation to all arguments of a variadic template. Here's the sum function using a fold expression:

template <typename... Ts>
auto sum(Ts... args) {
    return (args + ...);
}

Fold expressions can greatly simplify the code for variadic templates.

Advanced Examples and Use Cases

Type Traits and SFINAE

Type traits and SFINAE (Substitution Failure Is Not An Error) are advanced techniques used to make decisions at compile time based on types. Here's an example that uses SFINAE to enable a function only for integral types:

#include <type_traits>

template <typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
increment(T value) {
    return value + 1;
}

int main() {
    std::cout << increment(5) << std::endl; // Outputs 6
    // std::cout << increment(5.5) << std::endl; // Compilation error
    return 0;
}

CRTP (Curiously Recurring Template Pattern)

The CRTP is a technique where a class template derives from itself. It's commonly used for static polymorphism. Here's an example:

template <typename Derived>
class Base {
public:
    void interface() {
        static_cast<Derived*>(this)->implementation();
    }
};

class Derived : public Base<Derived> {
public:
    void implementation() {
        std::cout << "Derived implementation" << std::endl;
    }
};

int main() {
    Derived d;
    d.interface(); // Outputs "Derived implementation"
    return 0;
}

Wrapping Up (and a Promise)

I'm just scratching the surface here, but hopefully, you've got a taste of the amazing things you can do with modern C++ templates. From basic function and class templates to advanced techniques like variadic templates, template metaprogramming, and concepts, templates are a powerful tool in your C++ arsenal.

I promise to try and be a more regular presence on this blog, sharing more code snippets, tips, and tricks. Until next time, happy coding!

0
Subscribe to my newsletter

Read articles from Enzo Hugonnier directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Enzo Hugonnier
Enzo Hugonnier