C++ Generics

Don’t be (too) clever

- Bjarne Stroustrup

Reusability

I’m sure you’ve seen or written code like the following yourself:

float sum (float a, float b) {
    return a + b;
}

int sum (int a, int b) {
    return a + b;
}

Or what about something like this?

class IntArray {
    size_t size{};
    int *numbers{};
public:
    IntArray() : size(1), numbers(new int[1]) {}
    ~IntArray() noexcept { delete[] numbers; }
};
class FloatArray {
    size_t size{};
    float *numbers{};
public:
    FloatArray() : size(1), numbers(new float[1]) {}
    ~FloatArray() noexcept { delete[] numbers; }
};

Wouldn’t it be nice to avoid having to repeat so much code? Well, you can probably think of one or two ways of doing it. But importantly, let’s discuss one that works at compile time.


The Basics

1. Defining a Template

Here’s the basic format:

template <template-parameters> declaration;

template-parameters:

class | typename [identifier] [=default-value]

eg. <class T, class U>

Templates can be declared for

  • classes / structs
  • functions
  • type aliases
  • variables
  • concepts

Here is how we can convert the first example:

template <class T>
T sum(T a, T b) {
    return a + b;
}

(Note: class and typename are interchangeable keywords)

Or the second:

template <typename T>
class DynamicArray {
    size_t size{};
    T* numbers{};
public:
    DynamicArray() : size(1), numbers(new T[1]) {}
    ~DynamicArray() noexcept { delete[] numbers; }
};

Now we can use our new templates and the compiler will generate code for each required type.

int i = sum(2, 3);
float f = sum(3.0f, 4.0f);

DynamicArray<int> iarray{};
DynamicArray<float> farray{};

Important note! This generated code will take up space and can be an important factor for larger projects or smaller systems. Always consider the tradeoffs in your choices.

Class and function templates might be the most common uses of C++ generics, but as mentioned above, there are some other valuable features to know.

2. Other Important Uses

As a Type Alias:

template <class T> using ptr = T*;

As a Variable:

template <typename T>
constexpr bool is_big_and_trivial =
    sizeof(T) > 16 &&
    std::is_trivially_copyable<T>::value &&
    std::is_trivially_destructible<T>::value;

3. More About Template Parameters

Template parameters can be a bit more complex than I’ve shown so far.

Above, I specifically used type template parameters. class|typename [identifier] [=default-value]

However, there are also non-type template parameters… type|auto [identifier] [=default-value]

…and template template parameters. template <template-parameters> class|typename [identifier]

Here are some examples of both of these new template parameters.

If we want to create a function with a non-type template parameter:

template <int Value>
int multiply(int x) {
    return x * Value;
}

int main() {
    std::cout << "5 * 3 = " << multiply<3>(5) << std::endl;
}

Or if we want to change our Array class to be a fixed size:

template <typename T, size_t N>
class FixedArray {
private:
    T data[N];
public:
    constexpr size_t size() const { return N; }

    T& operator[](size_t i) { return data[i]; }
    const T& operator[](size_t i) const { return data[i]; }
};

int main() {
    FixedArray<int, 5> arr; // Size (5) is a compile-time constant
    arr[0] = 10;
    std::cout << "Array size: " << arr.size() << std::endl;
    std::cout << "First element: " << arr[0] << std::endl;
}

And here is an example of using a template template parameter to change the container type of a stack:

template<class T, template <typename, typename> class Container>
class Stack {
private:
    Container<T, std::allocator<T>> data;
public:
    void push(const T& item) {
        data.push_back(item);
    }

    void pop() {
        if (!data.empty()) {
            data.pop_back();
        }
    }

    T& top() {
        return data.back();
    }

    bool empty() const {
        return data.empty();
    }
};

int main() {
    Stack<int, std::vector> stackWithVector;
    Stack<int, std::deque> stackWithDeque;
    Stack<int, std::list> stackWithList;
}

Here we use a previously declared template as a template-parameter, we just have to inform the compiler of the structure of this other template.

Variadic Template Parameters

Finally, all template parameters may be variadic.

Here are three examples, one with a base case, one using a fold expression, and one using sizeof...:

// Base case: no arguments
void print() {
    std::cout << "\n";
}

// Variadic template: takes any number of arguments
template<typename T, typename... Args>
void print(T first, Args... rest) {
    std::cout << first << " ";
    print(rest...);  // Recursive call with remaining arguments
}

// Another example: sum function
template<typename... Args>
auto sum(Args... args) {
    return (args + ...);  // C++17 fold expression
}

// Example with sizeof... operator
template<typename... Args>
void printCount(Args... args) {
    std::cout << "Number of arguments: " << sizeof...(args) << "\n";
}

int main() {
    // Print various types
    print(1, 2.5, "hello", 'x', true);
    
    // Sum numbers
    std::cout << "Sum: " << sum(1, 2, 3, 4, 5) << "\n";
    std::cout << "Sum: " << sum(1.5, 2.5, 3.0) << "\n";
    
    // Count arguments
    printCount(1, 2, 3, 4);
    printCount("a", "b");
    
    return 0;
}

Substitution and Instantiation

Substitution and Instantiation are important terms in C++ generic programming.

Substitution: Results in class or function declaration. Instantiation: Results in full definition of class, function, type alias or variable.

Substitution is done by the compiler to check for the correctness of the template arguments. Instantiation is done to check for the correctness of the definition.

Substitution

May occur on its own when either:

  • An incomplete type is sufficient
  • Class template partial specialization is required

Substitution results in an incomplete class type of which the contents are not checked.

  • Note: The function signature is checked, including parameters, return type, noexcept clause

After substitution, the best match among candidate template substitutions is chosen for instantiation.

Instantiation

Replaces template parameters with template arguments in the class definition.

Results in a complete class definition.

Member functions are not instantiated until they are used.

Class Template Example

Suppose we have the following class template:

template <class T, class U>
class Pair {
private:
    T m0;
    U m1;
public:
    Pair() {}
    Pair(T v0, U v1) : m0(v0), m1(v1) {}
    T first() const { return m0; }
    U second() const { return m1; }
};

And we write the following code:

Pair<int, data>

The compiler will behave as if the following exists in source code:

class PairIi4dataE {
private:
    int m0;
    data m1;
public:
    PairIi4dataE();
    PairIi4dataE(int v0, data v1);
    int first() const;
    data second() const;
};

But if we instead had written:

Pair<int[10], data>

The compiler will behave as if this exists:

class PairIA10_i4dataE {
private:
    int m0[10];
    data m1;
public:
    PairIA10_i4dataE();
    PairIA10_i4dataE(int v0[10], data v1);
    int (first())[10] const;                // <--- This is a problem!
    data second() const;
};

An instantiation error will fail at compile time.

Function Template Example

What about for template functions?

Suppose we had defined the following:

template <class T>
void swap_pointed_to(T* a, T* b) {
    T temp = *a;
    *a = *b;
    *b = temp;
}

And now we wish to use it:

swap_pointed_to<double>

We get something like this:

void swap_pointed_toIdEvPT_S1_(double* a, double* b) {
    double temp = *a;
    *a = *b;
    *b = temp;
}

Great! The compiler is doing the work for us, and will fail for us if instantiation fails.

What about if Substitution fails?

SFINAE

Substitution Failure Is Not An Error

Important and necessary for:

  • Overloading of function templates
  • Class template partial specialization

If there is a failure during substitution, the candidate is simply discarded.


I feel like at this point I’ve covered enough information for a single post. Stay tuned for Part 2 where I will dive into the second half of David Olsen’s talk, including Constraints, Specializations, the typename keyword, and some best practices.


Quick reference

// Function template
template <class T>
T sum(T a, T b) { return a + b; }

// Class template
template <typename T>
class Container { T value; };

// Type alias template
template <class T>
using ptr = T*;

// Variable template
template <typename T>
constexpr T pi = T(3.14159265358979);

// Non-type template parameter
template <size_t N>
class FixedBuffer { char data[N]; };

// Template template parameter
template <template <typename> class Container>
class Wrapper { Container<int> inner; };

// Variadic template
template <typename... Args>
auto sum_all(Args... args) { return (args + ...); }

// Substitution: compiler checks template arguments
// Instantiation: compiler generates full definition when used
// SFINAE: invalid substitutions are silently discarded

Take-aways and notes

  • Templates enable code reuse without sacrificing type safety or performance.
  • class and typename are interchangeable in template parameter lists.
  • Template instantiation generates code for each unique set of template arguments — be mindful of code bloat.
  • Non-type template parameters allow compile-time constants (sizes, values) as template arguments.
  • Template template parameters let you parameterize over container types or other templates.
  • Variadic templates enable functions and classes that accept any number of arguments.
  • Substitution vs instantiation: substitution checks the declaration; instantiation checks the full definition.
  • SFINAE is foundational for template metaprogramming and overload resolution.

📚 Further Reading