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
- Can this code be simplified?
- Yep. We don’t need both
printDoubles
and printLetters
.
- Both can be named
print
, because the compiler can disambiguate the
calls to print
by the types of the arguments.
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
- Can we do the same for the copy functions?
- Sure, why not?
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?
- Do you see further simplifications?
- No, unless there is some way to parameterize the type of the arguments
when you define the functions.
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
- The first template parameter is usually named
T
,
for type.
- The second template parameter is usually named
U
,
because U
comes after T
.
- 𝔸 𝔹 ℂ 𝔻 𝔼 𝔽 𝔾 ℍ 𝕀 𝕁 𝕂 𝕃 𝕄 ℕ 𝕆 ℙ ℚ ℝ 𝕊 𝕋 𝕌 𝕍 𝕎 𝕏 𝕐 ℤ
T1
& T2
are also common.
- Meaningful names are nice, for larger templates.
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
- The use of
T
in copy
is completely separate
from the use of T
in print
.
- Just like the
i
in copy
is separate
from the use of i
in print
.
- Scope is our friend.
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
copy
has a const in the arguments.
print
does not, though it probably should.
- There are four templatized function calls.
- What does
T
represent in each?
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
- Can we get rid of the duplications between
PointInt
and
PointDouble
?
- Only if there is a way to parameterize the type of the class.
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() {
// This statement fails—must specify the type.
// Point pi(1, 2);
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, despite appearances.
If it were a method of Handful
, would it need to be a
friend of Handful
?
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
4196032
0
749839456
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?
- a floating-point number
- a string (
"foo"
or "bar"s
)
- an object
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";
}
😀
- The template parameter is a typename.
- The actual argument is the type double.
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
- The formal parameter is a double.
- The actual argument is
3.16227766
.
Standard containers
- This is how the standard containers (the STL) are implemented.
- There’s nothing magic about them.
- <vector> contains, essentially:
template <typename T>
class vector {
…
};
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?
- classes
- functions
- using aliases
- variables
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 size() const { return sizeof(data); }
};
int main() {
Dozen<int> di;
Dozen<void> dv;
cout << di.size() << ' ' << dv.size() << '\n';
}
c.cc: In instantiation of 'class Dozen<void>':
c.cc:10: required from here
c.cc:3: error: creating array of 'void'
Sometimes, a template doesn’t work for all types.
Template specialization
You can have the general case, and a specialized case:
template <typename T> // general case
class Dozen {
T data[12];
public:
size_t size() const { return sizeof(data); }
};
template <> // this version applies
struct Dozen<void> { // only to void
size_t size() const { return 0; }
};
int main() {
Dozen<int> di;
Dozen<void> dv;
cout << di.size() << ' ' << dv.size() << '\n';
}
48 0