Class Templates
Are there MORE kinds of templates besides function templates?
Yes, there are class templates, too.
I think we've seen them already. When we say
std::vector<int>
etc., we're using a class template, right?That's right. Class templates are especially useful for data structures that might store different types of items.
Here's a simple class that stores a pair of int
s.
intpair.hpp
(include guard omitted):
class IntPair {
public:
IntPair(int f, int s);
int getFirst() const;
int getSecond() const;
void setFirst(int f);
void setSecond(int s);
private:
int first_;
int second_;
};
And here it is converted into a class template. Now when we want a pair like IntPair
, we can say Pair<int,int>
, but we can specify any types we want for the data it stores—it isn't locked to storing int
s (e.g., a Pair<std::string,bool>
will be possible too).
pair.hpp
(include guard omitted):
template <typename F, typename S>
class Pair {
public:
Pair(const F& first, const S& second);
const F& getFirst() const;
const S& getSecond() const;
void setFirst(const F& f);
void setSecond(const S& s);
private:
F first_;
S second_;
};
#include "pair-private.hpp"
pair-private.hpp
template <typename F, typename S>
Pair<F, S>::Pair(const F& first, const S& second) :
first_{first},
second_{second} {
}
template <typename F, typename S>
const F& Pair<F, S>::getFirst() const {
return first_;
}
template <typename F, typename S>
void Pair<F, S>::setFirst(const F& f) {
first_ = f;
}
Things to Notice
- When you define a member function, you need to say what class it's part of. Because these functions are part of a class template, that means…
- Every single member function definition needs to have the same template preamble as the class itself did.
- e.g., template
- e.g., template
- The class name in the function name needs template arguments!
- e.g.,
Pair<F, S>::
rather thanPair::
- e.g.,
- Every single member function definition needs to have the same template preamble as the class itself did.
And here is some code that uses the Pair
template.
main.cpp
:
#include "pair.hpp"
int main() {
Pair<float, bool> p1{4.2, true};
float x = p1.getFirst();
Pair<int, int*>* p2 = new Pair<int, int*>{6, nullptr};
int* y = p2->getSecond();
Pair<Pair<float, bool>, Pair<int, int*>*> p3{p1, p2};
delete p2;
}
Things to Notice
- There is no class named
Pair
!Pair
is a class template, and a class template is not a class, it's a recipe for making individual classes, such asPair<int,int>
.- The same is true for other class templates, such as
std::vector
andstd::list
. - A template name is not a type without template arguments.
- If you try to say
std::vector myVector;
you will get an error. - Instead, be specific, say:
std::vector<int> myVector;
But why can't C++ automatically figure out the types like it did for functions? I think we ought to be able to say
Pair p1{4.2, true};
and have it figure out that
p1
is aPair<double,bool>
.It can! Class-template–argument deduction is a feature that was added in C++17.
But in CS 70, we prefer to be explicit and say what we mean. Too much stuff happening automagically can be confusing.
And learning about when deduction works and when it doesn't is a whole other rabbit hole! Let's not get too into the weeds.
You Explore and Experiment!
You can run an expanded variation of the above code online by clicking on
Look over the code:
- Compare
intpair.hpp
withpair.hpp
. - Compare
intpair.cpp
withpair-private.hpp
. - Read over
main.cpp
and run the program.
Expanding the Code
Now expand the code by adding a combine()
member function that returns a value of type F
by using the +
operator to combine first_
and second_
. (What combine()
actually does will depend on the definition of +
for the types in the pair!)
You'll need to declare combine()
in pair.hpp
and implement it in pair-private.hpp
.
Once you've added it, you can try it out with code like
std::cout << "pair0.combine(): " << pair0.combine() << std::endl;
std::cout << "pair1.combine(): " << pair1.combine() << std::endl;
std::cout << "pair2.combine(): " << pair2.combine() << std::endl;
If you need a working version of combine()
, you can get one from
Now try adding:
std::cout << "pair3.combine(): " << pair3.combine() << std::endl;
to try to combine (3.14, 2.718)
with (4.2, true)
.
It produces an error:
In file included from pair.hpp:30:0,
from main.cpp:2:
pair-private.hpp: In instantiation of ‘F Pair<F, S>::combine() const [with F = Pair<double, double>; S = Pair<float, bool>]’:
>main.cpp:75:55: required from here
pair-private.hpp:45:19: error: no match for ‘operator+’ (operand types are ‘const Pair’ and ‘const Pair’)
return first_ + second_;
~~~~~~~^~~~~~~~~
The compiler is saying that it can't stamp out a combine
function when F
is Pair<double, double>
and S
is Pair<float, bool>
because there is no +
operator for these two argument types.
Meh. An error from C++. I'm not surprised.
Hay! I am a bit surprised, actually. Because it only errored out when we added that line. Why is it only erroring out now? Our class template always had a
combine()
function; we weren't using it, but it was there. Why did it only blow up when we wrote code that used a function that we already had?C++'s template system is lazy. It only instantiates templates when it has to. So even though it had stamped out the
Pair<Pair<double, double>, Pair<float, bool>>
class, it hadn't bothered to make that class'scombine()
function until you tried to use it.Laziness is an idea I can get behind.
So now you've seen class templates… The syntax is a bit clunky, but it's quite flexible and powerful.
This all seems pretty straightforward.
Uh… Okay… Can we have a bigger example? Maybe of a whole class?
Sure. Let's convert our
IntVector
class.
(When logged in, completion status appears here.)