This project has been created as part of the 42 curriculum by baelgadi.
Templates
I can hold anything. The Soul-Self takes the shape of whatever I put inside it and it is never the thing it carries.
A template is written once, for nothing in particular, and takes the shape of whatever type we hand it.
It only ever asks one question of that type: “can you do the few things I need?”
|
CPP Module 07:
3 exercises. C++98 standard only. Compiler: c++ · Flags: -Wall -Wextra -Werror · Must compile with -std=c++98Exercise 00: Start with a few functions · ex00/ · whatever.hpp, swap / min / maxExercise 01: Iter · ex01/ · iter.hpp, a template over an array and over a functionExercise 02: Array · ex02/ · Array.hpp + Array.tpp, a class template that owns memoryForbidden: using namespace (-42) · friend (-42) · printf · *alloc · free · STL containers · STL algorithms · template definitions in a .cppRequired: every template lives in a header · Orthodox Canonical Form on Array<T> · allocation with new[] only · no leaks · operator[] throws when the index is out of bounds |
It’s a recipe for one; nothing exists until a type shows up.
template <typename T>
void swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}Call swap(a, b) on two int and the compiler deduces T = int then writes an entire swap(int&, int&) into the binary.
Call it on std::string and it writes a second one.
Call it on Emotion and it writes a third.
3 real functions, one source; for the template itself never gets compiled into anything.
That deduction is why we never write swap<int>(a, b) in ex00. (the arguments already say what T is)
| C macro | void * |
Template | |
|---|---|---|---|
| Type checked? | No, textual substitution | No, you cast and pray | Yes, fully |
| One version per type? | Zero, it’s just text | One version, blind | One generated per type |
Works with std::string? |
Badly | No, it has a destructor | Yes |
| Cost at runtime | None | An indirection per access | None |
| Cost elsewhere | Debugging | Correctness | Binary size, compile time |
The catch is when the type checking happens.
A template body is really only checked at the moment someone instantiates it:
template <typename T>
const T& min(const T& a,const T& b)
{
return ((a < b) ? a : b);
}Nothing here says T must be comparable, and yet it must.
T needs exactly the operations the body names, and not one more.
[!NOTE] Pass a type with no
operator<and the error does not point at your call, it points into the template body at a line you did not write today.
(Read those errors bottom-up: the last frame is the instantiation that caused it)
3 functions, one header, no .cpp.
ex00/whatever.hpp is the whole exercise.
swap takes T& because it has to reach the caller’s variables.
min and max take const T& and give back const T&: no copy on the way in, no copy on the way out. In the case of an int that’s noise, but for a std::string or an Emotion it’s a constructor call we didn’t pay for.
template <typename T>
const T& max(const T& a, const T& b)
{
return((a > b) ? a : b);
}Now the part some people may get wrong. When the 2 values are equal:
a < b |
a > b |
Returned | |
|---|---|---|---|
min(a, b) with a == b |
false | - | b |
max(a, b) with a == b |
- | false | b |
That’s why the check in ex00/main.cpp prints addresses rather than values:
std::cout << "&min(e, f) = " << &::min(e, f) << GRAY << " (must be &f)" << RESET << std::endl;The :: is not decoration either.
::swap says the one at global scope, mine, which keeps std::swap out of the discussion no matter <string>.
What would a class, say Emotion, actually have to provide?
| Function | What it needs from T |
|---|---|
swap |
A copy constructor and a copy assignment |
min |
operator< |
max |
operator> |
[!NOTE]
Emotioncompares on_intensityand prints_name, somax(rage(100), calm(7))printsrage(100).
Which field a type considers “greater” is the type’s business, never the algorithm’s.
iter is a template over 2 things at once: what’s in the array and what to do with it.
template <typename T, typename F>
void iter(T *array, const size_t length, F func)
{
if (array == NULL)
return ;
for (size_t i = 0; i < length; i++)
func(array[i]);
}F is left completely open.
Never void (*)(T&), just F. So anything that can be written as func(x) is accepted which in C++98 means a function pointer or a class with operator().
Then, right underneath, a second one:
template <typename T>
void iter(T *array, const size_t length, void (*func)(const T &))That overload exists because of the way main.cpp calls it:
::iter(tab, 5, print);print is itself a template. Passing its bare name hands the compiler a whole family of functions, not one single function.
With only the F version there is nothing to pick from; as F cannot be deduced from an overload set that hasn’t yet been resolved.
The second overload gives it a concrete shape to match against so T = int is deduced from tab, void (*)(const int &) is built from it and print<int> is the one member of the family that fits!
| Call | What is passed | Resolved by |
|---|---|---|
::iter(tab, 5, print) |
A template name, unresolved | The void (*)(const T&) overload |
::iter(tab, 5, increment<int>) |
A concrete function, int& |
The generic F overload |
::iter(tab, 5, printInt) |
A concrete function, const int& |
The void (*)(const T&) overload |
::iter(words, 3, toUpper) |
A concrete function, std::string& |
The generic F overload |
::iter(tab, 5, print<int>) |
Explicitly instantiated | The generic F overload |
increment and toUpper take a non const reference, so they could never bind to the const T& overload.
They need the open F.
[!WARNING]
iteris notstd::transform. It returns nothing and owns nothing.
toUppertakesstd::string&and rewrites the string where it lies.
iterpassesarray[i]straight through and never copies anything, so the array is changed after the call.
A .cpp is compiled alone.
If Array<T>’s constructor is defined in Array.cpp and main.cpp asks for Array<int>, the compiler handling Array.cpp never learns that int was wanted, so it generates nothing. The compiler handling main.cpp knows int but has no body to stamp out.
Both files compile but the link fails.
undefined reference to `Array<int>::Array(unsigned int)'
So the definition has to be visible wherever it’s used (which means a header).
In ex02, Array.hpp declares the class and then pulls the bodies in at the very bottom:
# include "Array.tpp"
#endifAnd Array.tpp is not in the Makefile’s SRCS. It’s never compiled on its own since it’s only ever included.
[!IMPORTANT]
.tppis not a real C++ extension, it’s a convention. The compiler only sees a file that got #included.
That’s also why “no function implementation in a header” from the earlier modules does not apply here. For a template there is no other legal place.
[!NOTE] The include guard in
Array.tppand the# include "Array.hpp"at its top make the file safe to open, read, or include from either direction.
Array<T>, a container that never asksSame header, same body, and it holds int, std::string or Gem without a single line changing.
template <typename T>
Array<T>::Array(unsigned int n) : _data(new T[n]()), _size(n) {}Beware. The empty parentheses at the end of new T[n]() are the entire difference between a clean array and 750 pieces of garbage!
new T[n] |
new T[n]() |
|
|---|---|---|
T = int |
Uninitialised, whatever garbage was in that memory | Initialised to 0 |
T = std::string |
Default constructed | Default constructed |
T = Gem |
Default constructed | Default constructed |
A class always gets its default constructor called either way. But a builtin type does not; that is unless we ask.
Then there is the Orthodox Canonical Form, and here it earns its keep because the class owns a raw pointer:
template <typename T>
Array<T>& Array<T>::operator=(const Array& other)
{
if (this != &other)
{
delete[] _data;
_data = NULL;
_size = 0;
if (other._size > 0)
{
_data = new T[other._size]();
_size = other._size;
}
for (unsigned int i = 0; i < _size; i++)
_data[i] = other._data[i];
}
return *this;
}The elements are copied one by one with T’s own operator=.
Bounds checking sits in both subscript operators:
template <typename T>
T& Array<T>::operator[](unsigned int index)
{
if (index >= _size)
throw std::out_of_range("Array: index out of bounds");
return _data[index];
}One returns T& so arr[0] = x works, the other returns const T& so a const Array<int> can still be read.
std::out_of_range comes from <stdexcept> and derives from std::exception so the CPP05 lightning rod still catches it:
catch(const std::exception& e)[!WARNING]
numbers[-2]in the subject’s test is not caught by a second check.
The parameter isunsigned int, so-2converts to4294967294before the function body even starts andindex >= _sizecatches it.
So what does Array<T> demand of T in the end?
| Requirement | Where it comes from |
|---|---|
| Default constructible | new T[n]() builds n of them |
| Copy assignable | _data[i] = other._data[i] |
| Destructible | delete[] _data |
That’s all. Not comparable, nor printable, nor copy constructible.
# ──── ex00 ────
# Verify: subject example prints a = 3, b = 2 and chaine2 / chaine1
# Verify: with e == f, &min(e, f) and &max(e, f) both print &f (the second arg)
cd ex00 && make
./whatever
valgrind --leak-check=full ./whatever
# ──── ex01 ────
# Verify: the same iter call prints ints, Runes, const ints and strings
# Verify: length 0 and a NULL array print nothing and do not crash
cd ex01 && make
./iter
valgrind --leak-check=full ./iter
# ──── ex02 ────
# Verify: "subject test: OK"
# Verify: Array<int> ints(5) prints five zeros, Array<std::string> prints empty strings
# Verify: deep copy
# Verify: self assignment keeps content intact
cd ex02 && make
./array
valgrind --leak-check=full ./arraystd::out_of_rangeA template is not code. It’s the instructions for writing code and the compiler is the one holding the pen.
In the end, there really is no end. Just new beginnings.