Examine the program below:
#include <iostream>
struct IntVertex {
int x {};
int y {};
};
struct DoubleVertex {
double x {};
double y {};
};
struct IntTriangle {
IntVertex a {};
IntVertex b {};
IntVertex c {};
};
struct DoubleTriangle {
DoubleVertex a {};
DoubleVertex b {};
DoubleVertex c {};
};
void print(IntTriangle triangle)
{
std::cout << "(" << triangle.a.x << ", " << triangle.a.y << ")\n";
std::cout << "(" << triangle.b.x << ", " << triangle.b.y << ")\n";
std::cout << "(" << triangle.c.x << ", " << triangle.c.y << ")\n";
}
void print(DoubleTriangle triangle)
{
std::cout << "(" << triangle.a.x << ", " << triangle.a.y << ")\n";
std::cout << "(" << triangle.b.x << ", " << triangle.b.y << ")\n";
std::cout << "(" << triangle.c.x << ", " << triangle.c.y << ")\n";
}
int main()
{
IntTriangle pixelTriangle {
.a = { 0, 0 },
.b = { 10, 0 },
.c = { 5, 8 }
};
DoubleTriangle worldTriangle {
.a = { 0.0, 0.0 },
.b = { 2.5, 0.0 },
.c = { 1.25, 3.75 }
};
print(pixelTriangle);
std::cout << '\n';
print(worldTriangle);
}
// Terminal output:
(0, 0)
(10, 0)
(5, 8)
(0, 0)
(2.5, 0)
(1.25, 3.75)
We have to duplicate the struct and function just to support another type. What if we want the program to be compatible with more types like float?
Clearly this doesn’t scale. We can do better.
Class Templates
A class template is a template definition for instantiating class types. It’s similar to function overloading in that a template can support many different types. This saves you having to duplicate the code to suit a different type.
Let’s rewrite the above program to use a class template
NOTE: Structs are used but this also applies to classes
A class template starts with a template parameter declaration:
template<typenameT>
The placeholder type can then be used inside the class template:
template <typename T>
struct Vertex {
T x {};
T y {};
};
template <typename T>
struct Triangle {
Vertex<T> a {};
Vertex<T> b {};
Vertex<T> c {};
};
T is not a real type yet. It is a placeholder for the coordinate type.
When we write: Triangle<int> the compiler generates a concrete Triangle type where each vertex stores int coordinates:
When we write: Triangle<double> the compiler generates a different concrete type where each vertex stores double coordinates.
#include <iostream>
template <typename T>
struct Vertex {
T x {};
T y {};
};
template <typename T>
struct Triangle {
Vertex<T> a {};
Vertex<T> b {};
Vertex<T> c {};
};
template <typename T>
void print(const Triangle<T>& triangle)
{
std::cout << "(" << triangle.a.x << ", " << triangle.a.y << ")\n";
std::cout << "(" << triangle.b.x << ", " << triangle.b.y << ")\n";
std::cout << "(" << triangle.c.x << ", " << triangle.c.y << ")\n";
}
int main()
{
Triangle<int> pixelTriangle {
.a = { 0, 0 },
.b = { 10, 0 },
.c = { 5, 8 }
};
Triangle<double> worldTriangle {
.a = { 0.0, 0.0 },
.b = { 2.5, 0.0 },
.c = { 1.25, 3.75 }
};
std::cout << "Pixel triangle:\n";
print<int>(pixelTriangle);
}
// Output
Pixel triangle:
(0, 0)
(10, 0)
(5, 8)
Instead of four structs, we have two. Instead of two print() functions, we have one.
<typename> communicates the template types that are compatible with these template. T in the <> is the template type.
When instantiating Triangle<int>, the compiler generates a concrete Triangle type by substituting T with int, so each Vertex<T> member becomes a Vertex<int>.
Class templates with template type and non-template type members
It’s perfectly fine to have a template with non-T types. In the example below, the second member will always be int.
template <typename T>
struct Vertex {
T x {};
int y {};
};
Extend class templates by using multiple template types
There is no need to confine ourselves to one type. By expanding the class template with another template parameter, it can support an additional type!
#include <iostream>
template <typename X, typename Y>
struct Vertex {
X x {};
Y y {};
};
template <typename X, typename Y>
struct Triangle {
Vertex<X, Y> a {};
Vertex<X, Y> b {};
Vertex<X, Y> c {};
};
template <typename X, typename Y>
void print(const Triangle<X, Y>& triangle)
{
std::cout << "(" << triangle.a.x << ", " << triangle.a.y << ")\n";
std::cout << "(" << triangle.b.x << ", " << triangle.b.y << ")\n";
std::cout << "(" << triangle.c.x << ", " << triangle.c.y << ")\n";
}
int main()
{
Triangle<int, double> screenTriangle {
.a = { 0, 0.5 },
.b = { 10, 0.75 },
.c = { 5, 8.25 }
};
Triangle<double, int> gridTriangle {
.a = { 0.25, 0 },
.b = { 2.5, 0 },
.c = { 1.25, 4 }
};
std::cout << "Screen triangle:\n";
print(screenTriangle);
}
// Terminal output:
Screen triangle:
(0, 0.5)
(10, 0.75)
(5, 8.25
Triangle<int,double> means the x coordinate uses int, while the y coordinate uses double.
Making a function template work with more than one class type
In the previous example, print() was written specifically for Triangle<T> objects:
template <typename T>
void print(const Triangle<T>& triangle)
{
std::cout << "(" << triangle.a.x << ", " << triangle.a.y << ")\n";
std::cout << "(" << triangle.b.x << ", " << triangle.b.y << ")\n";
std::cout << "(" << triangle.c.x << ", " << triangle.c.y << ")\n";
}
This works, but it is narrow. The function can only accept Triangle<T> objects.
We can make the function template more flexible by letting the entire object type be deduced.
#include <iostream>
#include <string_view>
template <typename T>
struct Vertex {
T x {};
T y {};
};
template <typename T>
struct Triangle {
Vertex<T> a {};
Vertex<T> b {};
Vertex<T> c {};
};
struct DebugTriangle {
Vertex<int> a {};
Vertex<int> b {};
Vertex<int> c {};
std::string_view label {};
};
template <typename Shape>
void printVertices(const Shape& shape)
{
std::cout << "(" << shape.a.x << ", " << shape.a.y << ")\n";
std::cout << "(" << shape.b.x << ", " << shape.b.y << ")\n";
std::cout << "(" << shape.c.x << ", " << shape.c.y << ")\n";
}
int main()
{
Triangle<double> worldTriangle {
.a = { 0.0, 0.0 },
.b = { 2.5, 0.0 },
.c = { 1.25, 3.75 }
};
DebugTriangle debugTriangle {
.a = { 0, 0 },
.b = { 10, 0 },
.c = { 5, 8 },
.label = "hitbox"
};
printVertices(worldTriangle);
std::cout << '\n';
}
// Terminal output:
(0, 0)
(2.5, 0)
(1.25, 3.75)
Ultimately
A class template lets you write one reusable class definition and instantiate it for different types:
Class templates promote code reusability and is the gateway to generic programming.