Skip to content
  • Class Templates & Specialisation · C++

Class Templates & Specialisation

LearnViaNotes @learnvianotes· 3 min read

Class templates are how the STL is built: std::vector<T>, std::map<K, V>, std::array<T, N>. The type parameter becomes part of the class's type.

A generic container

C++
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>

template <typename T>
class Stack {
public:
    void push(const T& value) { items_.push_back(value); }
    void pop() {
        if (items_.empty()) throw std::out_of_range("pop on empty stack");
        items_.pop_back();
    }
    const T& top() const { return items_.back(); }
    bool empty() const { return items_.empty(); }
    std::size_t size() const { return items_.size(); }
private:
    std::vector<T> items_;
};

int main() {
    Stack<int> s;
    s.push(1); s.push(2); s.push(3);
    s.pop();
    std::cout << s.top() << ' ' << s.size() << '\n';

    Stack<std::string> words;
    words.push("hello");
    std::cout << words.top() << '\n';
}
Output
2 2
hello

Defaults, non-type parameters and CTAD

C++
#include <array>
#include <iostream>
#include <utility>
#include <vector>

template <typename T, std::size_t Capacity = 4>
class RingBuffer {
    std::array<T, Capacity> data_{};
    std::size_t head_ = 0, count_ = 0;
public:
    void push(T v) {
        data_[(head_ + count_) % Capacity] = v;
        if (count_ < Capacity) ++count_; else head_ = (head_ + 1) % Capacity;
    }
    T front() const { return data_[head_]; }
    std::size_t size() const { return count_; }
};

int main() {
    RingBuffer<int, 3> rb;
    for (int i = 1; i <= 5; ++i) rb.push(i);       // keeps the last 3
    std::cout << rb.front() << ' ' << rb.size() << '\n';

    std::pair p{1, 2.5};                            // CTAD (C++17): pair<int, double>
    std::vector v{1, 2, 3};                         // vector<int>
    std::cout << p.second << ' ' << v.size() << '\n';
}
Output
3 3
2.5 3

Full and partial specialisation

C++
#include <iostream>

template <typename T>
struct TypeName { static const char* get() { return "unknown"; } };

template <>                                   // full specialisation
struct TypeName<int> { static const char* get() { return "int"; } };

template <typename T>                         // partial specialisation: any pointer
struct TypeName<T*> { static const char* get() { return "pointer"; } };

template <typename K, typename V>
struct Pair { const char* kind() const { return "general"; } };
template <typename T>
struct Pair<T, T> { const char* kind() const { return "same types"; } };   // partial

int main() {
    std::cout << TypeName<int>::get() << ' ' << TypeName<double*>::get() << ' ' << TypeName<char>::get() << '\n';
    std::cout << Pair<int, double>{}.kind() << ", " << Pair<int, int>{}.kind() << '\n';
}
Output
int pointer unknown
general, same types
Function templatesClass templates
Full specialisationYesYes
Partial specialisationNo (overload instead)Yes
Deduce argumentsYes (from call)C++17 CTAD (from constructor)

Member templates & typename

C++
#include <iostream>
#include <list>
#include <vector>

template <typename Container>
typename Container::value_type first(const Container& c) {   // `typename`: a dependent *type*
    return *c.begin();
}

struct Printer {
    template <typename T>
    void print(const T& x) const { std::cout << x << '\n'; }   // member function template
};

int main() {
    std::cout << first(std::vector<int>{7, 8}) << ' ' << first(std::list<char>{'z'}) << '\n';
    Printer{}.print(3.5);
}
Output
7 z
3.5

Why do you sometimes need the typename keyword inside a template?

What is the difference between full and partial specialisation?

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report