Class templates

A class template is a class with a type parameter, and std::vector is the one everybody uses. The parameter can be the type of the stored element, the type of a key, or the size of a fixed array, and the compiler generates a distinct class for every set of arguments.

Lesson 037 applied the idea to functions. This lesson applies it to classes: the definition, the out-of-class member syntax, default arguments, and the deduction that C++17 added at the construction site.

A class with a type parameter

The class below stores one value of type T and offers get() and set(). Every member function that uses T is a template member, but inside the class body T is just a name and no extra syntax is needed.

The program creates Box<int> and Box<std::string> and prints both values. The two classes are independent: Box<int> has an int member, Box<std::string> a std::string member, and there is no common base.

The angle brackets in the declaration are part of the type. Box<int> and Box<double> are as different as int and double, and a function that takes Box<int> cannot be called with Box<double>.

The compiler generates the class on demand, member by member: a Box<T> that is only constructed and never read still gets its constructor, but a member function that is never called is not instantiated at all. That laziness lets a template support types that cannot do everything the class body mentions, as long as the unused parts stay unused.

#include <iostream> #include <string> template <typename T> class Box { public: explicit Box(T value) : value_(value) {} const T& get() const { return value_; } void set(const T& value) { value_ = value; } private: T value_; }; int main() { Box<int> number{42}; Box<std::string> text{"hello"}; number.set(7); std::cout << number.get() << ' ' << text.get() << '\n'; }

g++ -std=c++23 -Wall -Wextra class.cpp -o class_tmpl
./class_tmpl

7 hello

A member defined outside the class

A member function can be defined outside the class body, and for a class template the definition needs the template header again plus the class name with its parameter: template <typename T> T Box<T>::get() const.

The program defines both members outside the class and uses them. The syntax looks heavy, which is why short functions stay inside the class body and long ones move out.

A member defined outside the class in the same header is implicitly inline, so a header-only template does not break the one definition rule of lesson 006.

#include <iostream> template <typename T> class Box { public: explicit Box(T value); T get() const; private: T value_; }; template <typename T> Box<T>::Box(T value) : value_(value) { } template <typename T> T Box<T>::get() const { return value_; } int main() { Box<int> b{9}; std::cout << b.get() << '\n'; }

g++ -std=c++23 -Wall -Wextra outside.cpp -o outside
./outside

9

A default template argument

A template parameter can have a default, written after the parameter name. The caller then names only the arguments that differ, and a class template can have a sensible common form without losing the general one.

The program's Pair has a default second type of int, so Pair<double> means Pair<double, int>. The printed values show the two members and their types through the arithmetic.

Default arguments must come last in the parameter list, as with function defaults. Lesson 013 covered the same rule for functions.

#include <iostream> template <typename First, typename Second = int> class Pair { public: Pair(First first, Second second) : first_(first), second_(second) {} First first() const { return first_; } Second second() const { return second_; } private: First first_; Second second_; }; int main() { Pair<double> a{1.5, 2}; Pair<int, long> b{3, 4L}; std::cout << a.first() + a.second() << ' ' << b.first() + b.second() << '\n'; }

g++ -std=c++23 -Wall -Wextra default.cpp -o default_tmpl
./default_tmpl

3.5 7

A container template

The useful class template holds a collection, and the standard containers are the finished version. The stack below stores its elements in a std::vector<T> and offers push, pop and top; the vector does the memory work.

The program pushes three ints and pops one, then does the same with strings. Each instantiation gets its own vector, sized for its own element type.

This is the shape of every container adaptor in the standard library, and lesson 057 shows the ready-made one. The value here is seeing that a template member can use T anywhere a type is allowed.

#include <iostream> #include <vector> template <typename T> class stack { public: void push(const T& value) { items_.push_back(value); } void pop() { items_.pop_back(); } const T& top() const { return items_.back(); } std::size_t size() const { return items_.size(); } private: std::vector<T> items_; }; int main() { stack<int> numbers; numbers.push(1); numbers.push(2); numbers.push(3); numbers.pop(); stack<const char*> words; words.push("one"); words.push("two"); std::cout << numbers.top() << ' ' << numbers.size() << ' '; std::cout << words.top() << ' ' << words.size() << '\n'; }

g++ -std=c++23 -Wall -Wextra stack.cpp -o stack_tmpl
./stack_tmpl

2 2 two 2

Class template argument deduction

Since C++17 the compiler can deduce the template arguments of a class from its constructor arguments. Pair p{1.5, 2} builds a Pair<double, int> without the angle brackets, and Box b{42} builds a Box<int>.

The deduction can fail, and the message is longer than the function version because the compiler lists the constructors it tried. The program below declares a Box with no argument and no type, and the compiler cannot decide what T is.

Class template argument deduction only helps at the construction site: a variable declared with the type still needs the full name, and a function parameter written as Box without arguments is not a type.

#include <iostream> template <typename T> class Box { public: explicit Box(T value) : value_(value) {} T get() const { return value_; } private: T value_; }; int main() { Box b; std::cout << b.get() << '\n'; }

g++ -std=c++23 -Wall -Wextra deduction.cpp -o deduction

deduction.cpp: In function 'int main()':
deduction.cpp:20:9: error: class template argument deduction failed:
20 | Box b;
| ^
deduction.cpp:20:9: error: no matching function for call to 'Box()'
deduction.cpp:20:9: note: there are 2 candidates
deduction.cpp:7:14: note: candidate 1: 'template<class T> Box(T) -> Box<T>'
7 | explicit Box(T value) : value_(value) {}
| ^~~
deduction.cpp:7:14: note: candidate expects 1 argument, 0 provided
deduction.cpp:4:7: note: candidate 2: 'template<class T> Box(Box<T>) -> Box<T>'
4 | class Box
| ^~~
deduction.cpp:4:7: note: candidate expects 1 argument, 0 provided

Exercises

  1. Add a second type parameter to Box and store two values of different types.
  2. Give stack a top() that returns a copy instead of a reference and explain which call in the program would change.
  3. Write `Box b{42};` and check which type the compiler deduces. Then declare a Box and see whether the same deduction works.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What is a class template?

    Answer

    A class with a type or value parameter

    The parameter makes the class a pattern, and each set of arguments gives a distinct class.

  2. How is a member defined outside its class template?

    Answer

    With template <typename T> before the qualified name

    Each member definition repeats the template header and the Box<T>:: qualification.

  3. What is a default template argument?

    Answer

    A type or value used when the caller leaves the parameter out

    The default makes Box<int> and Box<int, 4> both usable from one primary template.

  4. What does class template argument deduction use?

    Answer

    The constructor arguments, through a deduction guide

    CTAD lets Box b{7} deduce T from the constructor, when a guide or an implicit one matches.

  5. What does this program print? #include <iostream> #include <string> template <typename T> class box { public: explicit box(T v) : value_(v) {} T get() const { return value_; } private: T value_; }; int main() { box<int> a{7}; box<std::string> b{"hello"}; std::cout << a.get() << ' ' << b.get(); }

    Answer

    7 hello

    One instantiation stores an int and another a string, and the program prints both values.

  6. What does this program print? #include <iostream> template <typename T> class box { public: explicit box(T v) : value_(v) {} T squared() const; private: T value_; }; template <typename T> T box<T>::squared() const { return value_ * value_; } int main() { box<int> b{3}; std::cout << b.squared(); }

    Answer

    9

    The out-of-class member is instantiated for int, and it returns 3 squared.

  7. What does this program print? #include <iostream> template <typename T = double> struct pair { T first; T second; }; int main() { pair<> a{3.5, 7}; std::cout << a.first << ' ' << a.second; }

    Answer

    3.5 7

    The default argument makes the type double, and 7 is converted to it.

  8. What does this program print? #include <iostream> #include <string> #include <vector> template <typename T> class stack { public: void push(T value) { data_.push_back(value); } T top() const { return data_.back(); } std::size_t size() const { return data_.size(); } private: std::vector<T> data_; }; int main() { stack<int> numbers; numbers.push(1); numbers.push(2); stack<std::string> words; words.push("two"); std::cout << numbers.size() << ' ' << numbers.top() << ' ' << words.top() << ' ' << words.size(); }

    Answer

    2 2 two 1

    The two instantiations keep their own elements, and the print shows the sizes and the tops.

  9. What does Box b; report when only a converting constructor exists?

    Answer

    class template argument deduction failed

    There is no argument to deduce T from, and the guide needs one.

  10. Why is a class template defined in a header?

    Answer

    The compiler instantiates it where it is used

    Each translation unit that uses the template needs the definition to generate its version.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev