This lesson covers an advanced topic that provides powerful capabilities for specific scenarios. Partial template specialization allows you to specialize a template for categories of types while keeping other template parameters flexible.

The limitation of full specialization

Consider a 2D table template with both type and dimension parameters:

template <typename T, int Rows, int Cols>
class Table
{
private:
    T m_cells[Rows][Cols]{};

public:
    T& at(int row, int col)
    {
        return m_cells[row][col];
    }

    const T& at(int row, int col) const
    {
        return m_cells[row][col];
    }

    T* data() { return &m_cells[0][0]; }
};

Suppose you want a print function that displays tables. A generic version:

template <typename T, int Rows, int Cols>
void print(const Table<T, Rows, Cols>& table)
{
    for (int r{ 0 }; r < Rows; ++r)
    {
        for (int c{ 0 }; c < Cols; ++c)
            std::cout << table.at(r, c) << ' ';
        std::cout << '\n';
    }
}

This works for numeric types:

#include <iostream>

int main()
{
    Table<int, 3, 4> numbers;

    int value{ 1 };
    for (int r{ 0 }; r < 3; ++r)
        for (int c{ 0 }; c < 4; ++c)
            numbers.at(r, c) = value++;

    print(numbers);

    return 0;
}

Output:

1 2 3 4
5 6 7 8
9 10 11 12

But for character tables, you might prefer no spaces between characters for a more compact display:

int main()
{
    Table<char, 3, 10> text;

    const char* msg{ "HelloWorld" };
    for (int i{ 0 }; msg[i]; ++i)
        text.at(0, i) = msg[i];

    print(text);  // Shows: H e l l o W o r l d (with spaces - not ideal)

    return 0;
}

The problem with full specialization

You might try full specialization:

template <>
void print<char, 10, 3>(const Table<char, 10, 3>& table)
{
    for (int r{ 0 }; r < 3; ++r)
    {
        for (int c{ 0 }; c < 10; ++c)
            std::cout << table.at(r, c);
        std::cout << '\n';
    }
}

This only works for Table<char, 10, 3>. What about Table<char, 5, 2> or Table<char, 20, 4>? You would need a separate specialization for every dimension combination - impractical.

Partial template specialization

Partial template specialization solves this by letting you specialize some parameters while leaving others as templates:

#include <iostream>

template <typename T, int Rows, int Cols>
class Table
{
private:
    T m_cells[Rows][Cols]{};

public:
    T& at(int row, int col) { return m_cells[row][col]; }
    const T& at(int row, int col) const { return m_cells[row][col]; }
};

template <typename T, int Rows, int Cols>
void print(const Table<T, Rows, Cols>& table)
{
    for (int r{ 0 }; r < Rows; ++r)
    {
        for (int c{ 0 }; c < Cols; ++c)
            std::cout << table.at(r, c) << ' ';
        std::cout << '\n';
    }
}

// Overload for char tables - works for any dimensions
template <int Rows, int Cols>
void print(const Table<char, Rows, Cols>& table)
{
    for (int r{ 0 }; r < Rows; ++r)
    {
        for (int c{ 0 }; c < Cols; ++c)
            std::cout << table.at(r, c);
        std::cout << '\n';
    }
}

int main()
{
    Table<char, 3, 10> text;
    const char* line1{ "Hello" };
    const char* line2{ "World" };

    for (int i{ 0 }; line1[i]; ++i)
        text.at(0, i) = line1[i];
    for (int i{ 0 }; line2[i]; ++i)
        text.at(1, i) = line2[i];

    print(text);  // Uses char overload - no spaces

    std::cout << '\n';

    Table<int, 2, 4> numbers;
    numbers.at(0, 0) = 10;
    numbers.at(0, 1) = 20;
    numbers.at(1, 0) = 30;
    numbers.at(1, 1) = 40;

    print(numbers);  // Uses generic template - with spaces

    return 0;
}

Output:

Hello
World

10 20 0 0
30 40 0 0

The char-specific version:

template <int Rows, int Cols>
void print(const Table<char, Rows, Cols>& table)
  • Still has template parameters (Rows and Cols)
  • Fixes char for the type parameter
  • Works for char tables of any size

Partial specialization is for classes only

An important restriction: you can only partially specialize class templates, not standalone function templates. The function example above works because we are overloading (not specializing) print with a different template signature.

For true partial specialization, use class templates:

#include <iostream>

// Primary template
template <typename T, int Rows, int Cols>
class Board
{
private:
    T m_cells[Rows][Cols]{};

public:
    void set(int r, int c, const T& value) { m_cells[r][c] = value; }
    const T& get(int r, int c) const { return m_cells[r][c]; }

    void show() const;
};

template <typename T, int Rows, int Cols>
void Board<T, Rows, Cols>::show() const
{
    for (int r{ 0 }; r < Rows; ++r)
    {
        for (int c{ 0 }; c < Cols; ++c)
            std::cout << m_cells[r][c] << ' ';
        std::cout << '\n';
    }
}

// Partial specialization for bool type
template <int Rows, int Cols>
class Board<bool, Rows, Cols>
{
private:
    bool m_cells[Rows][Cols]{};

public:
    void set(int r, int c, bool value) { m_cells[r][c] = value; }
    bool get(int r, int c) const { return m_cells[r][c]; }

    void show() const;
};

template <int Rows, int Cols>
void Board<bool, Rows, Cols>::show() const
{
    for (int r{ 0 }; r < Rows; ++r)
    {
        for (int c{ 0 }; c < Cols; ++c)
            std::cout << (m_cells[r][c] ? 'X' : '.') << ' ';
        std::cout << '\n';
    }
}

int main()
{
    Board<int, 2, 3> numBoard;
    numBoard.set(0, 0, 1);
    numBoard.set(0, 1, 2);
    numBoard.set(1, 2, 3);
    numBoard.show();

    std::cout << '\n';

    Board<bool, 3, 3> gameBoard;
    gameBoard.set(0, 0, true);
    gameBoard.set(1, 1, true);
    gameBoard.set(2, 2, true);
    gameBoard.show();

    return 0;
}

Output:

1 2 0
0 0 3

X . .
. X .
. . X

Member functions cannot be partially specialized

You cannot directly partially specialize a member function. Consider this attempt:

// This doesn't compile
template <int Rows, int Cols>
void Board<double, Rows, Cols>::show() const
{
    std::cout << std::fixed << std::setprecision(2);
    // ...
}

This fails because show() is a member of Board. To customize it for double, you must specialize the entire class:

template <int Rows, int Cols>
class Board<double, Rows, Cols>
{
private:
    double m_cells[Rows][Cols]{};

public:
    void set(int r, int c, double value) { m_cells[r][c] = value; }
    double get(int r, int c) const { return m_cells[r][c]; }

    void show() const;
};

template <int Rows, int Cols>
void Board<double, Rows, Cols>::show() const
{
    std::cout << std::fixed << std::setprecision(2);
    for (int r{ 0 }; r < Rows; ++r)
    {
        for (int c{ 0 }; c < Cols; ++c)
            std::cout << m_cells[r][c] << ' ';
        std::cout << '\n';
    }
}

This duplicates the entire class. To reduce duplication, use inheritance with a common base:

#include <iostream>
#include <iomanip>

template <typename T, int Rows, int Cols>
class BoardBase
{
protected:
    T m_cells[Rows][Cols]{};

public:
    void set(int r, int c, const T& value) { m_cells[r][c] = value; }
    const T& get(int r, int c) const { return m_cells[r][c]; }

    void show() const
    {
        for (int r{ 0 }; r < Rows; ++r)
        {
            for (int c{ 0 }; c < Cols; ++c)
                std::cout << m_cells[r][c] << ' ';
            std::cout << '\n';
        }
    }
};

template <typename T, int Rows, int Cols>
class Board : public BoardBase<T, Rows, Cols>
{
};

template <int Rows, int Cols>
class Board<double, Rows, Cols> : public BoardBase<double, Rows, Cols>
{
public:
    void show() const
    {
        std::cout << std::fixed << std::setprecision(2);
        for (int r{ 0 }; r < Rows; ++r)
        {
            for (int c{ 0 }; c < Cols; ++c)
                std::cout << this->m_cells[r][c] << ' ';
            std::cout << '\n';
        }
    }
};

int main()
{
    Board<int, 2, 2> intBoard;
    intBoard.set(0, 0, 5);
    intBoard.set(1, 1, 7);
    intBoard.show();

    std::cout << '\n';

    Board<double, 2, 2> doubleBoard;
    doubleBoard.set(0, 0, 3.14159);
    doubleBoard.set(1, 1, 2.71828);
    doubleBoard.show();

    return 0;
}

Output:

5 0
0 7

3.14 0.00
0.00 2.72

Note: The this-> prefix is required when accessing base class members in a template-dependent context.

Practical use cases

Partial specialization is useful for:

  1. Type categories: Specializing for all pointer types, all integral types, etc.
  2. Dimension handling: Different implementations for 1D vs 2D containers
  3. Algorithm optimization: Different strategies based on type properties
  4. Storage adaptation: Specialized memory layouts for specific types

Example: Specializing for all pointer types

Create a class template that handles pointers differently:

#include <iostream>

template <typename T>
class Handler
{
public:
    static void process(T value)
    {
        std::cout << "Value: " << value << '\n';
    }
};

template <typename T>
class Handler<T*>
{
public:
    static void process(T* ptr)
    {
        if (ptr)
            std::cout << "Pointer to: " << *ptr << '\n';
        else
            std::cout << "Null pointer\n";
    }
};

int main()
{
    Handler<int>::process(42);

    int x{ 100 };
    Handler<int*>::process(&x);
    Handler<int*>::process(nullptr);

    return 0;
}

Output:

Value: 42
Pointer to: 100
Null pointer

The Handler<T*> specialization handles all pointer types, not just int*. This pattern works for double*, char*, or any other pointer type.

Summary

Partial template specialization allows you to:

  1. Specialize some template parameters while leaving others flexible
  2. Create type-category specializations (e.g., all pointer types)
  3. Avoid writing separate full specializations for every combination

Key limitations:

  • Only class templates can be partially specialized, not function templates
  • Member functions cannot be partially specialized directly
  • Use inheritance with a common base to share code between specializations

Partial specialization is an advanced feature best reserved for situations where full specialization or function overloading proves inadequate.