Overloading operators and function templates

Function templates allow us to write generic code that works with multiple types. However, when we instantiate a template with a custom class type, the template may try to use operators that our class doesn't support. This is where operator overloading and templates work together.

The problem: templates and missing operators

Consider this simple function template:

template <typename T>
const T& maximum(const T& a, const T& b)
{
    return (a > b) ? a : b;
}

This works fine with built-in types like int and double, but what about custom classes?

#include <iostream>

class Distance
{
private:
    int m_meters{};

public:
    explicit Distance(int meters) : m_meters{meters} {}

    int getMeters() const { return m_meters; }
};

template <typename T>
const T& maximum(const T& a, const T& b)
{
    return (a > b) ? a : b;
}

int main()
{
    Distance run1{5000};
    Distance run2{7500};

    Distance longer{maximum(run1, run2)};  // Compilation error!
    std::cout << longer.getMeters() << "m\n";

    return 0;
}

The compiler complains: "no match for 'operator>' in the comparison." The template tries to use operator> on Distance objects, but we haven't defined it!

The solution: overload the necessary operators

To make our Distance class work with the maximum template, we need to overload operator>:

#include <iostream>

class Distance
{
private:
    int m_meters{};

public:
    explicit Distance(int meters) : m_meters{meters} {}

    friend bool operator>(const Distance& d1, const Distance& d2)
    {
        return d1.m_meters > d2.m_meters;
    }

    friend std::ostream& operator<<(std::ostream& out, const Distance& d)
    {
        out << d.m_meters << "m";
        return out;
    }

    int getMeters() const { return m_meters; }
};

template <typename T>
const T& maximum(const T& a, const T& b)
{
    return (a > b) ? a : b;
}

int main()
{
    Distance run1{5000};
    Distance run2{7500};

    Distance longer{maximum(run1, run2)};
    std::cout << "Longer run: " << longer << '\n';

    return 0;
}

Output:

Longer run: 7500m

Now the template works! When the compiler instantiates maximum<Distance>, it finds our overloaded operator> and uses it.

A more complex example: computing averages

Let's create a template that computes the average of an array:

#include <iostream>

template <typename T>
T computeAverage(const T* array, int length)
{
    T sum{0};

    for (int i{0}; i < length; ++i)
        sum += array[i];

    sum /= length;
    return sum;
}

int main()
{
    int intArray[]{10, 20, 30, 40, 50};
    std::cout << "Integer average: " << computeAverage(intArray, 5) << '\n';

    double doubleArray[]{1.5, 2.5, 3.5, 4.5};
    std::cout << "Double average: " << computeAverage(doubleArray, 4) << '\n';

    return 0;
}

This works with built-in types. Now let's try it with a custom class:

#include <iostream>

class Rating
{
private:
    int m_stars{};

public:
    explicit Rating(int stars = 0) : m_stars{stars} {}

    friend std::ostream& operator<<(std::ostream& out, const Rating& r)
    {
        out << r.m_stars << " stars";
        return out;
    }

    int getStars() const { return m_stars; }
};

template <typename T>
T computeAverage(const T* array, int length)
{
    T sum{0};

    for (int i{0}; i < length; ++i)
        sum += array[i];

    sum /= length;
    return sum;
}

int main()
{
    Rating ratings[]{Rating{5}, Rating{4}, Rating{5}, Rating{3}};
    std::cout << computeAverage(ratings, 4) << '\n';  // Won't compile!

    return 0;
}

The compiler generates errors because Rating doesn't support:

  • Rating{0} construction from an integer (for T sum{0})
  • operator+= for adding ratings
  • operator/= for dividing by an integer

Let's fix this:

#include <iostream>

class Rating
{
private:
    int m_stars{};

public:
    explicit Rating(int stars = 0) : m_stars{stars} {}

    friend std::ostream& operator<<(std::ostream& out, const Rating& r)
    {
        out << r.m_stars << " stars";
        return out;
    }

    Rating& operator+=(const Rating& r)
    {
        m_stars += r.m_stars;
        return *this;
    }

    Rating& operator/=(int divisor)
    {
        m_stars /= divisor;
        return *this;
    }

    int getStars() const { return m_stars; }
};

template <typename T>
T computeAverage(const T* array, int length)
{
    T sum{0};

    for (int i{0}; i < length; ++i)
        sum += array[i];

    sum /= length;
    return sum;
}

int main()
{
    Rating ratings[]{Rating{5}, Rating{4}, Rating{5}, Rating{3}, Rating{4}};
    std::cout << "Average rating: " << computeAverage(ratings, 5) << '\n';

    return 0;
}

Output:

Average rating: 4 stars

Perfect! We implemented only the operators that the template actually uses:

  • Constructor taking int (for T sum{0})
  • operator+= (for accumulating)
  • operator/= (for dividing by count)
  • operator<< (for output)

Summary

Templates and operator requirements: Function templates can only use operations that are defined for the template type parameter. When instantiating a template with a custom class, you must provide overloaded operators for all operations the template uses.

Compile-time checking: Template errors are caught when the template is instantiated with a specific type, not when the template itself is defined. This means you'll see errors when you try to use an incompatible type with a template.

Minimal operator overloading: You only need to overload the operators that the template actually uses. Analyze the template implementation to determine which operators are required (e.g., operator>, operator+=, operator/=).

Template compatibility without modification: Making a class work with templates doesn't require changing the template code. Simply implement the necessary operators in your class, and the template will work automatically.

Best Practice
Before using a template with your custom class, understand what operators it needs. Implement only those operators, ensuring they have sensible meanings for your class. Test thoroughly if your class is meant to work with templates.