Lecture 12¶
Templates in C++: Passing Types¶
C++ provides a powerful technique called templates that let you pass a type to a function instead of a value.
As an example, consider these two functions:
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
void swap(string& a, string& b) {
string temp = a;
a = b;
b = temp;
}
The only difference between these functions is that one uses the type int
,
and the other uses the type string
. Templates let you pass the type to the
function so you need write only one function to handle both cases:
template<class T>
void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
// ...
int x = 3;
int y = 4;
swap(x, y); // T is int
string s = "cat";
string t = "dog";
swap(s, y); // T is string
The C++ compiler infers at compile-time what the value of the template
variable T` ought to be by examining the types of the variables passed to
``swap
. This generic swap
function works with any type of value that has
assignment defined for it.
Generic Algorithms¶
C++’s standard template library (STL) implements numerous fundamental data
structures and algorithms in this way. For example, here is how the find
function in the STL is often implemented:
template<class InputIt, class T>
InputIt find(InputIt first, InputIt last, const T& value)
{
for (; first != last; ++first) {
if (*first == value) {
return first;
}
}
return last;
}
The template here has two input types: one called InputIt
(an input
iterator type), and T
(the type of the objects being searched). In C++,
iterator types are essentially objects that act like pointers, i.e. iterators
are objects that refer to other objects, and can be compared, incremented, and
de-referenced like regular pointers.
The parameters to find
are two iterators, first
and last
, that
specify a range of objects. Many programmers find this to be an unusual way of
calling at algorithm, but with practice it soon becomes natural. Plus, this
approach turns out to be extremely flexible, allow you to, for instance,
easily search sub-sequence of an array or vector or any other container where
that makes sense.
For example, here is how you can use the STL sort
function to sort the
“middle” part of an array and a vector:
int arr[] = {7, 9, 1, -4, 0, 2, 2};
vector<int> vec = {8, 1, 1, 2, 8, 9, 0, 1};
sort(arr + 1, arr + 6);
for(int x : arr) cout << x << ' ';
cout << '\n';
sort(vec.begin() + 1, vec.end() - 1);
for(int x : vec) cout << x << ' ';
Generic Containers¶
Another important application of templates in C++ is to create type-safe
collections of objects. For example, vector<T>
is the type of a vector
containing just objects of type T
. Type-safe means that you will get a
compiler-time error if you try to put an object that is not of type T
in
it.
Smart Pointers¶
Templates are also used to implement other features. For instance, here is
standard C++ smart pointer called unique_ptr
:
unique_ptr<int> p(new int(5));
cout << *p << endl;
*p = 2;
cout << *p << endl;
p
acts similarly to a raw pointer, except it is guaranteed to have sole
ownership of the object being pointed to. Thus, you cannot make another
pointer point to the object p
owns:
unique_ptr<int> q = p; // compiler error
When p
goes out of scope, it automatically deletes it object to avoid
memory leaks. Since no other pointer points to it, there is no problem with
“double deletion”, or dangling pointers.
Comments on Templates¶
An example of the power (and complexity!) of templates, a technique known as template meta-programming can be used to make C++ templates perform any calculation at compile-time. This actually has practical uses, such as for fix-sized vectors, or doing small matrix computations.
While many basic examples of using templates are easy to understand and use, it turns out that C++ templates have many subtle rules that make them one of the most complex features in C++.
A significant problem with C++ templates in practice is that the error messages they can create, sometimes even for simple errors, can be hundreds, or even thousands of characters long (see the The Grand C++ Error Explosion Competition for some pathological examples of errors — the winner resulted in an error message almost six billion times the size of the program that produced it). This makes it quite hard to debug template errors.
Other languages, such as Java, C#, and Ada, implement the equivalent of templates, with various different rules, restrictions, and performance characteristics.
Closures¶
Closures are a subtle but powerful concept that take some time for most programmers to fully understand. The definition of function closure is simple: a clojure is a function plus the referencing environment for that function (i.e. a table of variables and values for all the non-local variables referenced in the function).
Closures can be quite useful in practice, and so many modern languages support them. For example, C++, Go, C#, JavaScript, Dart, Python, and Clojure all support function closures.
For example, consider the function make-adder
that returns a function:
(defn make-adder [n]
(fn [x] (+ x n))
)
You could use it like this:
=> (def add1 (make-adder 1))
#'user/add1
=> (add1 5)
6
The expression (fn [x] (+ x n))
is a lambda function, i.e. a function
with no name. What’s interesting about it is that the variable n
is not
defined anywhere inside of it. Instead, the n
in the lambda function is
bound to the value of the n
passed to make-adder
. Thus, strictly
speaking, make-adder
is returning a closure, not a function.
Here’s another example of closure, this time written in Python 3:
def make_counter(): # Python 3
count = -1
def counter():
nonlocal count
count += 1
return count
return counter
You could use it like this:
>>> next = make_counter()
>>> next()
0
>>> next()
1
>>> next()
2
>>> next()
3
make_counter
returns a closure: the inner function counter()
refers to
the variable count
, which is local to make_counter
, but not local to
counter
. So make_counter
can’t return just a function: it needs to
return a function plus a variable to store the value of count
.
Every time make_counter()
is called, a new closure is returned, so each
new counter has its own personal copy of count
, e.g.:
>>> next = make_counter()
>>> up = make_counter()
>>> next()
0
>>> next()
1
>>> up()
0
Coroutines¶
Another useful extension of the idea of a function is a coroutine. Roughly, a coroutine is a function that can be suspended and resumed at multiple different points.
For example, Python supports a form of coroutine (sometimes called a semi- coroutine) call generators. For example:
def counter_gen(): # Python
count = 0
while True:
yield count # yield instead of return
count += 1
You can use it like this:
>>> counter = counter_gen()
>>> counter.next()
0
>>> counter.next()
1
>>> counter.next()
2
When counter.next()
is called, the body code of counter_gen
is
executed until it reaches the yield
statement. When yield
is
encountered, the generator pauses and returns the value of count
. The next
time counter.next()
is called, the generator resumes execution with the
first statement after the yield
it has at paused at. Again, it runs until
it hits yield
again.
Since a Python generator does not do any work until .next()
is called,
they are often written using infinite loops. This can simplify the code. For
example, this generator yields the Fibonacci numbers:
def fib_gen():
yield 1
yield 1
a, b = 1, 1
while True:
a, b = b, a + b
yield b
You use it like this:
>>> fib = fib_gen()
>>> fib.next()
1
>>> fib.next()
1
>>> fib.next()
2
>>> fib.next()
3
>>> fib.next()
5
>>> fib.next()
8
One way to think about fib
is that it is an infinite set of numbers that
calculates its values on-demand. Thus, this generator never calculates more
than is asked for.
Here’s another example that uses recursion:
import copy
def nbit_gen(n):
if n <= 0:
yield []
else:
n1bits0 = [b for b in nbit_gen(n-1)]
n1bits1 = copy.deepcopy(n1bits0)
for s in n1bits0:
s.insert(0, 0)
yield s
for s in n1bits1:
s.insert(0, 1)
yield s
You can use it like this:
>>> bits = nbit_gen(3)
>>> for b in bits: print b
[0, 0, 0]
[0, 0, 1]
[0, 1, 0]
[0, 1, 1]
[1, 0, 0]
[1, 0, 1]
[1, 1, 0]
[1, 1, 1]
You can also pass values back into a Python generator when it is resumed, but we won’t get into that here.
In practice, Python generators have proven to be quite useful. Compared to, say, closures, they are easier to understand and to use.