Class Templates in C++

Class templates enable us to parameterize a type. The standard library, including standard I/O streams, strings, vectors, and maps, relies heavily on class templates.

Just as with function templates, class templates are marked with the template keyword. We supply the template arguments when instantiating the class. Whenever we use a different template argument, the compiler generates a new class for us with the appropriate type.

Let’s create a templated point class that accepts a type parameter that we will simply call Type.

#pragma once
#include 
#include 

template<class Type>
class Point{
    private:
        Type _x, _y;
    public:
        Point(Type x, Type y) : _x(x), _y(y) {}
        Type x() const { return _x; }
        Type y() const { return _y; }
        void moveTo(Type x, Type y){
            _x = x;
            _y = y;
        }
        void moveBy(Type x, Type y){
            _x += x;
            _y += y;
        }
        std::string toString(){
            std::stringstream ss;
            ss << "(" << _x << ", " << _y << ")";
            return ss.str();
        }
        
};

Note that the implementation for the template class is in the definition. Note as well that member functions of a class template are themselves function templates, using the same template parameters. However, we do not need to pass template parameters when using template class methods. We simply pass the template parameter or paremeters into the class during instantiation.

When we use a template, it is called instantiating the template. A template instance is thus a a concrete class that the compiler creates by using the template arguments to the template definition. Template initialization is the realization of a template for a specific set of template arguments.

#include <iostream>
#include "pointclass.h"


int main(){
    
    Point a(42, 47);
    Point b(19.19, 3.14159);
    
    std::cout << a.toString() << std::endl;
    std::cout << b.toString() << std::endl;
    
    a.moveTo(73, 1701);
    b.moveBy(2.51, 3.3);
    
    std::cout << a.toString() << std::endl;
    std::cout << b.toString() << std::endl;
    
    return 0;
}

A template instantiation that is created for us by the compiler is called an implicit specialization. It is also possible to hand code specific instatiations for particular types, but that’s something to talk about some other time.

It’s straightforward enough to create a template class for a point. Now, let’s try something a bit more complicated; let’s create a class to handle rational numbers.

#pragma once
#include <string>
#include <sstream>
#include <stdexcept>

template<class Type>
class Rational{
    private:
        Type _numerator;
        Type _denominator;
    public:
        //constructors
        Rational() : _numerator(0), _denominator(1) {}
        Rational(Type numerator) : _numerator(numerator), _denominator(1) {}
        Rational(Type numerator, Type denominator);
        
        template<class ReturnType>
        ReturnType convert() { 
            return static_cast<ReturnType>(_numerator) / static_cast<ReturnType>(_denominator);
        }
        
        Type numerator() const { return _numerator; }
        Type denominator() const { return _denominator; }
        
        std::string toString();
        
        //overloaded assignment
        Rational<Type>& operator=(const Rational<Type> &a);
    
};


template<class Type>
Rational<Type>::Rational(Type numerator, Type denominator){
        if(denominator == 0){
            //throw exception b/c denominator is 0
            throw std::invalid_argument("received 0 value for denominator");
        } else {
            _numerator = numerator;
            _denominator = denominator;
        }
}

template<class Type>
std::string Rational<Type>::toString(){
    std::stringstream ss;
    ss << _numerator << "/" << _denominator; return ss.str(); } //overloaded functions template<class Type>
bool operator==(Rational<Type> const& a, Rational<Type> const& b){
    return (a.numerator() == b.numerator()) && (a.denominator() == b.denominator());
}

template<class Type>
Rational<Type>& Rational<Type>::operator=(const Rational<Type> &a){
    _numerator = a.numerator();
    _denominator = a.denominator();
    return *this;
}

Programming with templates can be frustrating at first, but it adds a lot of power and flexibility. A template alets us write a function or class once, and lets teh compiler generate actual functions and classes for different template arguments.

Leave a comment