Chapter 57: C++ 5
Outline:
- Templates
- Operator overloads
Templates
Let’s say we want to make a vector class, but we want a version that works with ints, floats, or doubles. In C++, templates let you define all of the pieces of a class (or function). Then, you can specify a type to instantiate that template. Templates can be used to create a variety of classes that are not related (except that they are quite similiar), yet share all the code in one place.
template <typename T>
struct t_vec3
{
T x, y, z;
};
t_vec3<float> a;
t_vec3<int> b;
t_vec3<double> c;
You can also make template methods and functions:
template <typename T>
void swap(T & x, T & y)
{
T temp = x;
x = y;
y = temp;
}
And you can perform specialization, which is where you specify exactly how a template should be instantiated for a particular type:
template <>
void swap<int>(int & x, int & y)
{
x ^= y;
y ^= x;
x ^= y;
}
Reference: Templates
Curiously Recurring Template Pattern
There is one somewhat curious way to use templates. Let’s say we have a class that we want to make singleton.
class GameManager
{
public:
static GameManager & Get()
{
static GameManager instance;
return instance;
}
void Update();
private:
GameManager() = default;
};
By making the construct private, we ensure that only methods of GameManager can create an instance
(new GameManager()
won’t work outside of the class).
But, we provide the Get()
method which can be used to get the one and only instance.
GameManager::Get().Update();
Let’s say we want to declare one “Singleton” class that can be inherited from to make a class singleton. We can do so like this:
template <typename T>
class Singleton
{
public:
static T & Get()
{
static T instance;
return instance;
}
};
Then, we use it like this:
class GameManager : public Singleton<GameManager>
{
public:
void Update();
private:
friend class Singleton<GameManager>;
GameManager() = default;
};
Singleton is the base class, but it needs to be able to “create” an instance of the derived class. So, we pass the typename back to Singleton as a template parameter.
We need to make Singleton<GameManager>
a friend of GameManager
so that it can access the private constructor.
It doesn’t matter that Singleton<GameManager>
is the base class (and it wouldn’t help if the constructor was protected either).
Reference: CRTP
Template Metaprogramming
The templating language is Turing-complete, which means it can be used to write any program. Essentially, you use the compiler to run some code for you (instead of just running the compiled executable). This used to be somewhat scary looking, though recent versions of C++ have made it a little easier (it’s still scary). Here’s a fundamental, simple example:
template <int n>
struct factorial {
enum { value = n * factorial<n - 1>::value };
};
template <>
struct factorial<0> {
enum { value = 1 };
};
Operator Overloads
Operator overloads let us specify how operators apply to our classes. If we write a vector class, obviously we can have this:
vec3 a, b;
a.Add(b);
But what about this?
vec3 a, b, c;
a = b + c;
It turns out that operators can be implemented for your classes just like a method:
struct vec3
{
// ...
void operator += (const vec3 & other)
{
x += other.x;
y += other.y;
}
};
Note that if you are implementing an operator that can be used in expressions, you likely want to return a temporary instance of your class:
struct vec3
{
// ...
vec3 operator + (const vec3 & other)
{
vec3 temp;
temp.x = x + other.x;
temp.y = y + other.y;
return temp;
}
};
This is why we use the funny <<
syntax for writing messages to cout
.
That is the bitshift left operator, it just is just overloaded for formatted output.
Note that for some operators, you can define it outside of the class. If we want to be able to do this:
vec3 x;
cout << x << endl;
… we need to be able to write the operator <<
that takes a std::ostream
on the left and our vec3
class on the right.
We can do so by writing this outside of our vec3
class definition:
ostream & operator << (ostream & stream, const vec3 & vec)
{
return stream << vec.x << " " << vec.y << " " << vec.z;
}
Note how it takes an returns a reference to an ostream
.
This shows that we can use something somewhat atypical as the return type of our operator overloads
(normally you might think that bitshift left returns an integral type).
It also shows how we can chain together long formatting expressions using this operator.
By the way, we can also write this operator overload within our vec3
class using the friend
keyword:
friend ostream & operator << (ostream & stream, const vec3 & vec)
{
return stream << vec.x << " " << vec.y << " " << vec.z;
}
The friend
keyword in this context makes a function that is not a member of the class (it’s a standard, global function).
It also has access to all protected and private fields of the class, but in this case our vec3
has none.
You can overload just about every operator there is, except for .
, ::
, and ?:
.
Note that you can, however, overload ->
.
This is what makes it possible to have “smart” pointer classes.
template <typename T>
class SmartPointer
{
private:
T * ptr;
int references = 0;
public:
T * operator ->()
{
return ptr;
}
SmartPointer()
{
references ++;
}
~SmartPointer()
{
if (-- references == 0)
{
delete ptr;
}
}
};
You can also overload the ()
operator to make things that look like functions:
template <int min, max>
struct random
{
int operator()
{
return rand() % (max - min) + min;
}
};
random r;
int x = r();
Because of the way this looks and works, it is called a Function Object or sometimes functor. The standard library sorting functions can take classes like this to implement predicates:
struct {
bool operator()(const myclass & a, const myclass & b) const
{
return a.field < b.field;
}
} customLess;
vector<myclass> s;
std::sort(s.begin(), s.end(), customLess);
But wait a minute - the post and pre increment operators are going to be impossible to tell apart!
Remember that prefix increment (++a
) increments and returns the incremented value,
whereas postfix increment (a++
) increments and returns the value before it was incremented.
How do we define both ++a
and a++
using this syntax?
The answer is that postfix increment/decrement is given a dummy int
argument that is not used:
struct vec2
{
vec2 & operator++() // prefix
{
x++;
y++;
return *this;
}
vec2 operator++(int) // postfix
{
vec temp(*this);
operator++(); // calls the pre-increment
return temp;
}
};
Reference: operator overloading