CS253: Software Development with C++

Spring 2023

Templates

Show Lecture.Templates as a slide show.

CS253 Templates

CS253 Templates

This presentation was stolen from Prof. Chuck Anderson.

Initial Code

double *copyDoubles(const double *src, int num) {
    double *dest = new double[num];
    for (int i=0; i<num; i++) dest[i] = src[i];
    return dest;
}

char *copyLetters(const char *src, int num) {
    char *dest = new char[num];
    for (int i=0; i<num; i++) dest[i] = src[i];
    return dest;
}

void printDoubles(const double *src, int num) {
    for (int i=0; i<num; i++) cout << src[i] << '\n';
}

void printLetters(const char *src, int num) {
    for (int i=0; i<num; i++) cout << src[i] << '\n';
}

int main() {
    double doubles[] = {0.1, 0.2, 0.3, 0.4};
    double *doubles2 = copyDoubles(doubles, 4);
    char *letters2 = copyLetters("bonehead", 8);
    printDoubles(doubles2, 4);
    printLetters(letters2, 8);
}
0.1
0.2
0.3
0.4
b
o
n
e
h
e
a
d

Simplify

Simplified

double *copyDoubles(const double *src, int num) {
    double *dest = new double[num];
    for (int i=0; i<num; i++) dest[i] = src[i];
    return dest;
}

char *copyLetters(const char *src, int num) {
    char *dest = new char[num];
    for (int i=0; i<num; i++) dest[i] = src[i];
    return dest;
}

void print(const double *src, int num) {
    for (int i=0; i<num; i++) cout << src[i] << '\n';
}

void print(const char *src, int num) {
    for (int i=0; i<num; i++) cout << src[i] << '\n';
}

int main() {
    double doubles[] = {0.1, 0.2, 0.3, 0.4};
    double *doubles2 = copyDoubles(doubles, 4);
    char *letters2 = copyLetters("bonehead", 8);
    print(doubles2, 4);
    print(letters2, 8);
}
0.1
0.2
0.3
0.4
b
o
n
e
h
e
a
d

Simplify Again

Simplified Again

double *copy(const double *src, int num) {
    double *dest = new double[num];
    for (int i=0; i<num; i++) dest[i] = src[i];
    return dest;
}

char *copy(const char *src, int num) {
    char *dest = new char[num];
    for (int i=0; i<num; i++) dest[i] = src[i];
    return dest;
}

void print(const double *src, int num) {
    for (int i=0; i<num; i++) cout << src[i] << '\n';
}

void print(const char *src, int num) {
    for (int i=0; i<num; i++) cout << src[i] << '\n';
}

int main() {
    double doubles[] = {0.1, 0.2, 0.3, 0.4};
    double *doubles2 = copy(doubles, 4);
    char *letters2 = copy("bonehead", 8);
    print(doubles2, 4);
    print(letters2, 8);
}
0.1
0.2
0.3
0.4
b
o
n
e
h
e
a
d

Simplified more?

Templates to the rescue!

template <typename T>
T *copy(const T *src, int num) {
    T *dest = new T[num];     // could use auto
    for (int i=0; i<num; i++) dest[i] = src[i];
    return dest;
}

template <class T>            // old-fashioned syntax
void print(T *src, int num) {
    for (int i=0; i<num; i++) cout << src[i] << '\n';
}

int main() {
    double doubles[] = {0.1, 0.2, 0.3, 0.4};
    double *doubles2 = copy(doubles, 4);
    char *letters2 = copy("bonehead", 8);
    print(doubles2, 4);
    print(letters2, 8);
}
0.1
0.2
0.3
0.4
b
o
n
e
h
e
a
d

T is a placeholder, a compile-time argument representing a type.

Templates to the rescue!

template <typename T>
T *copy(const T *src, int num) {
    T *dest = new T[num];     // could use auto
    for (int i=0; i<num; i++) dest[i] = src[i];
    return dest;
}

template <class T>            // old-fashioned syntax
void print(T *src, int num) {
    for (int i=0; i<num; i++) cout << src[i] << '\n';
}

int main() {
    double doubles[] = {0.1, 0.2, 0.3, 0.4};
    double *doubles2 = copy(doubles, 4);
    char *letters2 = copy("bonehead", 8);
    print(doubles2, 4);
    print(letters2, 8);
}
0.1
0.2
0.3
0.4
b
o
n
e
h
e
a
d

Templates to the rescue!

template <typename T>
T *copy(const T *src, int num) {
    T *dest = new T[num];     // could use auto
    for (int i=0; i<num; i++) dest[i] = src[i];
    return dest;
}

template <class T>            // old-fashioned syntax
void print(T *src, int num) {
    for (int i=0; i<num; i++) cout << src[i] << '\n';
}

int main() {
    double doubles[] = {0.1, 0.2, 0.3, 0.4};
    double *doubles2 = copy(doubles, 4);
    char *letters2 = copy("bonehead", 8);
    print(doubles2, 4);
    print(letters2, 8);
}
0.1
0.2
0.3
0.4
b
o
n
e
h
e
a
d

Templates to the rescue!

template <typename T>
T *copy(const T *src, int num) {
    T *dest = new T[num];     // could use auto
    for (int i=0; i<num; i++) dest[i] = src[i];
    return dest;
}

template <class T>            // old-fashioned syntax
void print(T *src, int num) {
    for (int i=0; i<num; i++) cout << src[i] << '\n';
}

int main() {
    double doubles[] = {0.1, 0.2, 0.3, 0.4};
    double *doubles2 = copy(doubles, 4);
    char *letters2 = copy("bonehead", 8);
    print(doubles2, 4);
    print(letters2, 8);
}
0.1
0.2
0.3
0.4
b
o
n
e
h
e
a
d

Templates to the rescue!

template <typename T>
T *copy(const T *src, int num) {
    T *dest = new T[num];     // could use auto
    for (int i=0; i<num; i++) dest[i] = src[i];
    return dest;
}

template <class T>            // old-fashioned syntax
void print(T *src, int num) {
    for (int i=0; i<num; i++) cout << src[i] << '\n';
}

int main() {
    double doubles[] = {0.1, 0.2, 0.3, 0.4};
    double *doubles2 = copy(doubles, 4);
    char *letters2 = copy("bonehead", 8);
    print(doubles2, 4);
    print(letters2, 8);
}
0.1
0.2
0.3
0.4
b
o
n
e
h
e
a
d

Classes

Let’s keep track of either integer (chessboard square) or real (latitude & longitude) 2D points:

class PointInt {
    int x, y;
  public:
    PointInt(int xarg, int yarg) : x(xarg), y(yarg) { }
    int getx() const { return x; }
    int gety() const { return y; }
};

class PointDouble {
    double x, y;
  public:
    PointDouble(double xarg, double yarg) : x(xarg), y(yarg) { }
    double getx() const { return x; }
    double gety() const { return y; }
};

int main() {
    PointInt pi(1, 2);
    PointDouble pd(3.4, 2.5);
    cout << '(' << pi.getx() << ',' << pi.gety() << ")\n"
         << '(' << pd.getx() << ',' << pd.gety() << ")\n";
}
(1,2)
(3.4,2.5)

You saw this coming

Template class

template <typename T>
class Point {
    T x, y;
  public:
    Point(T xarg, T yarg) : x(xarg), y(yarg) { }
    T getx() const { return x; }
    T gety() const { return y; }
};

int main() {
    Point<int> pi(1, 2);
    Point<double> pd(3.4, 2.5);

    cout << '(' << pi.getx() << ',' << pi.gety() << ")\n"
         << '(' << pd.getx() << ',' << pd.gety() << ")\n";
}
(1,2)
(3.4,2.5)

Multiple template parameters

You can have more than one parameter—just be sure you use the right type in the right place.

template <typename Tx, typename Ty>
class Point {
    Tx x;
    Ty y;
  public:
    Point(Tx xarg, Ty yarg) : x(xarg), y(yarg) { }
    Tx getx() const { return x; }
    Ty gety() const { return y; }
};

int main() {
    Point<int, int> pi(1, 2);
    Point<double, double> pd(3.4, 2.5);
    Point<int, double> pid(5, 6.42);
    cout << '(' << pi.getx()  << ',' << pi.gety()  << ")\n"
         << '(' << pd.getx()  << ',' << pd.gety()  << ")\n"
         << '(' << pid.getx() << ',' << pid.gety() << ")\n";
}
(1,2)
(3.4,2.5)
(5,6.42)

Integer template parameters

You can also include a constant integer to specify things like the size of an array, and can have a default value.

Example

template <typename T, int N = 5>
class Handful {                       // Much like std::array
    T data[N];
  public:
    void setData(int index, const T &item) { data[index] = item; }
    friend ostream& operator<< (ostream &out, const Handful<T, N> &h) {
        for (int i=0; i<N; i++)
            out << h.data[i] << '\n';
        return out;
    }
};

int main() {
    Handful<char, 3> h;
    h.setData(0, 'a');
    h.setData(1, 'b');
    h.setData(2, 'c');
    cout << h;
}
a
b
c

operator<< is not a method, even though it’s written inside class Handful { … }. If it were a method of Handful, would it need to be a friend of Handful? The friend makes it a free function, despite its location.

A less successful alternative

template <typename T, int N = 5>
class Handful {
    T data[N];
  public:
    void setData(int index, const T &item) { data[index] = item; }
    friend ostream& operator<< (ostream &out, const Handful<T, N> &h) {
        for (int i=0; i<N; i++)
            out << h.data[i] << '\n';
        return out;
    }
};

int main() {
    Handful<int> h;
    h.setData(0, 42);
    h.setData(1, 365);
    cout << h;
}
42
365
0
0
4196336

Range of template parameters

What can a template parameter be?

If the template parameter is typename or class, the actual parameter doesn’t have to be a built-in boring type like int or float. It can be a class type, or even filled-out templated type such as set<long>.

Range of template parameters

What can’t a template parameter be?

Remember, it’s a compile-time parameter: types, integer values, and pointers.

Understand the difference

This is fine:

template<typename T>
class Foo {
  public:
    T data[3] = {1,2,3};
};

int main() {
    Foo<double> f;
    cout << "😀\n";
}
😀

Understand the difference

This won’t work:

template<double D>  // 🦡
class Foo {
  public:
    double datum = D;
};

int main() {
    Foo<3.16227766> f;  // 🦡
    cout << "😟\n";
}
c.cc:1: error: ‘double’ is not a valid type for a template non-type 
   parameter

Standard containers

    template <typename T>
    class vector {
        T *data;
        size_t allocated, used;
      public:
        void push_back(const T &datum) {
        …
    };

Templated using declarations

A using declaration can be templated to create a new type:

template<class T>
using Counter = map<T, size_t>;

int main() {
    Counter<string> c;
    c["alpha"]++;
    c["beta"]++;
    c["alpha"]++;
    for (auto v : c)
        cout << v.first << ' ' << v.second << '\n';
}
alpha 2
beta 1

Templated variables

God help us, even variables can be templated:

template <typename T>
constexpr T pi = T(3.14159265358979323846264);

template <>
const string pi<string> = "π"s;

template <>
const string pi<const char *> = "π";

int main() {
    cout << pi<long double> << '\n'
         << pi<int> << '\n'
         << pi<string> << '\n';
}
3.14159
3
π

Responsibility

It is the user’s responsibility to ensure that the type actually makes sense:

template <typename T>
constexpr T pi = T(3.14159265358979323846264);

int main() {
    cout << pi<ofstream>;  // 🦡
}
c.cc: In instantiation of ‘constexpr const std::basic_ofstream<char> pi<std::basic_ofstream<char> >’:
c.cc:5:   required from here
c.cc:2: error: no matching function for call to ‘std::basic_ofstream<char>::
   basic_ofstream(double)’

What can be templated?

What can be templated?

Function specialization

You can have both a templated function and non-templated versions. No special syntax is needed—just write a version for the type that you want:

template <typename T>
void out(T n) {
    cout << "whatever: " << n << '\n';
}
void out(double n) {        // special case for double
    cout << "double: " << n << '\n';
}
void out(bool) = delete;    // forbid boolean argument

int main() {
    out(11);
    out(2.22);
    out(3.33F);
    out("chocolate");
}
whatever: 11
double: 2.22
whatever: 3.33
whatever: chocolate

The ordinary function is a better match than the templated function, so it gets used.

Template specialization

template <typename T>
class Dozen {
    T data[12] = {};
  public:
    size_t bytes() const { return sizeof(data); }
};

int main() {
    Dozen<bool> db;
    Dozen<string> ds;
    cout << db.bytes() << ' ' << ds.bytes() << '\n';
}
12 384

It’s a nice general template for an array of twelve whatevers.

Template specialization

template <typename T>
class Dozen {
    T data[12] = {};
  public:
    size_t bytes() const { return sizeof(data); }
};

int main() {
    Dozen<void> dv;  // 🦡
    cout << dv.bytes() << '\n';
}
c.cc: In instantiation of ‘class Dozen<void>’:
c.cc:9:   required from here
c.cc:3: error: creating array of ‘void’

Sometimes, a template doesn’t work for all types.

Template specialization

template <typename T>   // general case
class Dozen {
    T data[12] = {};
  public:
    size_t bytes() const { return sizeof(data); }
};

template <>             // this is template specialization
struct Dozen<void> {    // for void
    size_t bytes() const { return 0; }
};

int main() {
    Dozen<int> di;
    Dozen<void> dv;
    cout << di.bytes() << ' ' << dv.bytes() << '\n';
}
48 0

You can have the general case, and a specialized case. Did you notice the struct?

CTAD works with user-defined types, as well

Remember how CTAD figured out the type of a pair or vector from the initial values? That applies to your types, as well:

template <typename T>
class Point {
    T x, y;
  public:
    Point(T xarg, T yarg) : x(xarg), y(yarg) { }
    T getx() const { return x; }
    T gety() const { return y; }
};

int main() {
    Point i(01,23);   // note lack of <int,int>
    Point s("skidoo", "nertz");
    cout << i.gety() << ' ' << s.getx();
}
23 skidoo