Generic Classes and Advanced Template
Concepts
|
Learning
Objectives After completing this module,
students should be able to: •
Define and use class templates, including member functions
defined outside the class. •
Use several type parameters, non-type parameters and default
template arguments. •
Explain the use of the typename keyword for dependent names. •
Explain what the export keyword did, why it was removed, and what replaced
it. •
Describe the power of templates: specialization, compile-time
computation and generic algorithms. |
1. Generic Classes
Module 1 showed that a function can be written once for many types. The
same idea applies to classes. A generic class (class template) is
a class in which one or more data types are left as parameters; the compiler
generates an ordinary class from it for every type actually used.
A class template is useful for data structures and containers whose
behaviour does not depend on the type of the stored elements: stacks, queues,
linked lists, arrays, pairs and so on. Without templates a separate IntStack, DoubleStack
and CharStack would be needed.
1.1 Syntax
template <typename T>
class ClassName
{
T
data; // a member
whose type is the parameter
public:
void
setData(T value);
T getData()
const;
};
An object of a class template must name the type argument: ClassName<int> obj;.
Figure 2.1: A
class template is a blueprint; each type argument produces a separate class.
|
Important: Box
alone is not a type. Box<int> and Box<double>
are two different, unrelated classes generated from the same template. |
1.2 Program: a generic Box class
Program 2.1: A class template with one type
parameter.
#include <iostream>
#include <string>
using namespace std;
template <typename T>
class Box
{
private:
T value;
public:
void
setValue(T v) { value = v; }
T
getValue() const { return value; }
};
int main()
{
Box<int> b1;
Box<double> b2;
Box<string> b3;
b1.setValue(100);
b2.setValue(25.75);
b3.setValue("C++ Templates");
cout
<< "Integer: " << b1.getValue() << endl;
cout
<< "Double : " << b2.getValue() << endl;
cout
<< "String : " << b3.getValue() << endl;
return 0;
}
Output:
Integer: 100
Double : 25.75
String : C++ Templates
1.
template
<typename T> makes Box a
template, and T is used for the data member and
for the parameter and return type of the member functions.
2.
Box<int>
b1; makes the compiler create a class in which T
is int. Box<string>
works in the same way.
3.
getValue()
is declared const because it does not change
the object (good practice for accessor functions).
2. Defining Member Functions
Outside the Class
Member functions of a class template can be defined outside the class.
Two things are required: the template header must be repeated before
each definition, and the class name must be written with its parameter, Sample<T>::.
template <typename T>
return_type
ClassName<T>::functionName(parameters)
{
// body
}
Program 2.2: Constructor and member
functions defined outside the class template.
#include <iostream>
using namespace std;
template <typename T>
class Sample
{
private:
T value;
public:
Sample(T
x); // constructor
declaration
void
setValue(T x);
void
display() const;
};
/* Member functions defined outside: template header
+ Sample<T>:: */
template <typename T>
Sample<T>::Sample(T x) : value(x) {}
template <typename T>
void Sample<T>::setValue(T x)
{
value = x;
}
template <typename T>
void Sample<T>::display() const
{
cout
<< "Value: " << value << endl;
}
int main()
{
Sample<int> obj(50);
obj.display();
obj.setValue(75);
obj.display();
Sample<double> d(2.5);
d.display();
return 0;
}
Output:
Value: 50
Value: 75
Value: 2.5
|
Note: The out-of-class
definitions of a template are not ordinary function definitions; they
are part of the template. They must therefore stay in the same header file as
the class (or in a file included by it), otherwise the compiler cannot
instantiate them in other source files and the linker reports undefined
reference errors. |
3. Class Templates with Multiple
Type Parameters
A class template may have several type parameters, separated by commas,
so that members of different types can be stored together. The Standard Library
class std::pair<T1, T2> is such a
template.
Program 2.3: A Pair class template.
#include <iostream>
#include <string>
using namespace std;
template <typename T1, typename T2>
class Pair
{
private:
T1 first;
T2 second;
public:
Pair(T1 a,
T2 b) : first(a), second(b) {}
void
display() const
{
cout
<< "First value : " << first << endl;
cout
<< "Second value: " << second << endl;
}
};
int main()
{
Pair<int, double> p1(10, 3.14);
Pair<string, int> p2("Age", 25);
p1.display();
p2.display();
// C++17:
template arguments deduced (Pair<int, char>)
Pair p3(7,
'x');
p3.display();
return 0;
}
Output:
First value : 10
Second value: 3.14
First value : Age
Second value: 25
First value : 7
Second value: x
•
Pair<int,
double> stores an integer and a floating-point number; Pair<string, int> stores a name and a number.
•
The constructor uses a member initializer list, first(a), second(b), which is the preferred way to
initialise members.
|
Version Note: Class
template argument deduction (CTAD) is a C++17 feature.
In Program 2.3 the line Pair p3(7, 'x');
lets the compiler deduce Pair<int, char>
from the constructor arguments. Before C++17 the
template arguments had to be written, Pair<int, char> p3(7, 'x');
(or a helper function such as std::make_pair
was used). Compiled with -std=c++14,
the line produces: |
p2_pair.cpp:30:10: error: missing template arguments
before ‘p3’
4. Non-Type Parameters and
Default Template Arguments
Besides type parameters, a template may have non-type parameters:
constants whose values are supplied when the template is used, such as an
integer size. Template parameters can also have default arguments, just
like function parameters.
template <typename T, int SIZE = 5> // a type parameter and a non-type
parameter
class Stack { T arr[SIZE]; ... };
Program 2.4: A fixed-size stack whose
element type and capacity are template parameters.
#include <iostream>
using namespace std;
template <typename T, int SIZE = 5>
class Stack
{
private:
T
arr[SIZE];
int top;
public:
Stack() :
top(-1) {}
void push(T
value)
{
if (top
== SIZE - 1) {
cout << "Stack Overflow" << endl;
return;
}
arr[++top] = value;
}
void pop()
{
if (top
== -1) {
cout << "Stack Underflow" << endl;
return;
}
cout
<< "Removed: " << arr[top--] << endl;
}
void
display() const
{
if (top
== -1) {
cout << "Stack is empty" << endl;
return;
}
cout
<< "Stack elements: ";
for
(int i = top; i >= 0; i--)
cout << arr[i] << " ";
cout
<< endl;
}
};
int main()
{
Stack<int, 5> s;
s.push(10);
s.push(20);
s.push(30);
s.display();
s.pop();
s.display();
Stack<char, 2> cs;
// a different specialization
cs.push('a');
cs.push('b');
cs.push('c');
// overflow
Stack<double> ds;
// default argument: SIZE = 5
ds.pop();
// underflow
return 0;
}
Output:
Stack elements: 30 20 10
Removed: 30
Stack elements: 20 10
Stack Overflow
Stack Underflow
Figure 2.2: A
stack of three integers: push and pop work at the top.
•
The size is a compile-time constant; the array T arr[SIZE] is created without dynamic memory.
•
Stack<int,
5>, Stack<int, 10> and Stack<char, 2> are three different classes.
•
Stack<double>
uses the default SIZE = 5.
•
A non-type argument must be a constant expression; a
variable cannot be used.
|
Version Note: Non-type
parameters may be integers, enumerations,
pointers or references in all standards. C++17 adds template
<auto N>, where the type of N is deduced; C++20
additionally allows floating-point and certain class types. |
5. The typename
Keyword
The keyword typename has two
uses in templates.
5.1 Declaring a type parameter
template <typename T>
void show(T value);
Here typename and class are equivalent: template
<class T> means exactly the same.
5.2 Naming a dependent type
Inside a template, a name such as Container::value_type
depends on the template parameter. Before the template is instantiated
the compiler cannot know whether Container::value_type
is a type or, for example, a static data member. By default it assumes that it
is not a type. To tell the compiler that it is a type, the name is
preceded by typename.
Program 2.5: Using typename
for dependent type names.
#include <iostream>
#include <vector>
#include <list>
using namespace std;
/* Container::value_type is a dependent name that
names a type,
so it must
be preceded by the keyword typename. */
template <typename Container>
typename Container::value_type sumAll(const Container
&c)
{
typename
Container::value_type total = typename Container::value_type();
for
(typename Container::const_iterator it = c.begin(); it != c.end(); ++it)
total
+= *it;
return
total;
}
int main()
{
vector<int> v = {1, 2, 3, 4};
list<double> l = {1.5, 2.5, 3.0};
cout
<< "Sum of vector: " << sumAll(v) << endl;
cout
<< "Sum of list : "
<< sumAll(l) << endl;
return 0;
}
Output:
Sum of vector: 10
Sum of list :
7
Container::value_type is the
element type of the container and Container::const_iterator
is its iterator type. Without typename
the compiler would report an error such as need ‘typename’ before
‘Container::value_type’ because ‘Container’ is a dependent scope. The same
function works for vector<int>
and list<double>.
|
Version Note: C++17:
typename
may also be used in a template template parameter, template
<template <typename> typename C>. C++20
removes the need to write typename in some
contexts (for example, the return type of a member function declared in a
class), but writing it is still always correct. In examinations, use it
before every dependent type name. |
6. The export
Keyword
6.1 The original meaning (C++98)
Templates must normally be fully visible where they are instantiated, so
their definitions go into header files. The first C++ standard (C++98)
therefore added the keyword export
for templates, so that a template could be declared in a header and defined
in a separate source file, like an ordinary function:
// legacy C++98 only: exported template definition in
a .cpp file
export template <typename T>
T square(T x) { return x * x; }
The intention was to separate the interface from the implementation and
to reduce compilation time. In practice the feature was extremely hard to
implement: only a few compilers ever supported it, and the benefits were small.
|
Version Note: Removed
in C++11. The old meaning of export
template was removed from the language by the C++11
standard (the keyword was kept reserved). Current compilers do
not implement it. GCC 13 accepts the line above only to ignore it, with the
warnings: |
warning: keyword 'export' is deprecated, and is
ignored
warning: keyword 'export' not implemented, and will
be ignored
In modern C++ the practical rule is simple: write the full template
definition in a header file (or in a file that the header includes). If
compile time or code size matters, a template can be explicitly instantiated
in one source file for the chosen types, for example template
class Stack<int, 10>;.
6.2 The keyword in C++20: modules
|
Version Note: C++20
reuses the keyword export
for modules, which is a completely different
feature. export now marks the entities that a module
makes available to other files, including templates. Do not confuse it with
the removed C++98 export template. |
A module interface file (math_module.cppm)
exporting a template, and the file that imports it:
export module math_module;
export template <typename T>
T square(T x)
{
return x *
x;
}
#include <iostream>
import math_module;
int main()
{
std::cout
<< square(5) << " " << square(2.5) <<
std::endl;
return 0;
}
With GCC 13 these were built with:
g++ -std=c++20 -fmodules-ts -x c++ -c
math_module.cppm -o math_module.o
g++ -std=c++20 -fmodules-ts main_mod.cpp
math_module.o -o main_mod
Output: 25 6.25. Support
for modules still varies between compilers and build systems, so most course
programs continue to use header files.
7. The Power of Templates
Templates are one of the most powerful features of C++. They allow code
reuse, but they also support specialization, computation at compile time and
the design of generic libraries.
7.1 Generic data structures and algorithms
The Standard Template Library (STL) is built from class templates (vector, list, map, set)
and function templates (sort, find, min_element).
One sort works for every container of
a type that supports comparison, and one vector
stores any type.
7.2 Specialization of class templates
A class template can be specialized for particular types when the generic
code is not suitable. A full (explicit) specialization is written for
one exact type with template <>;
a partial specialization fixes only part of the parameters or a pattern,
such as “any pointer type”.
Program 2.6: Primary template, full
specialization for bool and partial
specialization for pointers.
#include <iostream>
using namespace std;
/* Primary template */
template <typename T>
class Printer
{
public:
void
print(const T &v) const { cout << "Value: " << v
<< endl; }
};
/* Full (explicit) specialization for bool */
template <>
class Printer<bool>
{
public:
void
print(const bool &v) const
{
cout
<< "Boolean: " << (v ? "true" :
"false") << endl;
}
};
/* Partial specialization for any pointer type T* */
template <typename T>
class Printer<T *>
{
public:
void
print(T *const &p) const
{
if (p)
cout << "Pointer to: " << *p << endl;
else cout << "Null
pointer" << endl;
}
};
int main()
{
int n = 42;
int *p =
&n;
int *q =
nullptr;
Printer<int>().print(7);
Printer<bool>().print(true);
Printer<int *>().print(p);
Printer<int *>().print(q);
return 0;
}
Output:
Value: 7
Boolean: true
Pointer to: 42
Null pointer
|
Use |
Template
chosen |
Reason |
|
Printer<int> |
Primary template |
No special version exists |
|
Printer<bool> |
Full specialization |
Exact match for bool: prints true/false |
|
Printer<int
*> |
Partial specialization Printer<T
*> |
Matches any pointer; prints the pointed-to value or reports
a null pointer |
|
Note: Function templates can
be fully specialized (Module 1, Section 6.2) but they cannot be partially
specialized; for functions, overloading is used instead. |
7.3 Computation at compile time
Because templates are expanded by the compiler, they can be used to
perform calculations during compilation (template metaprogramming).
The result is a constant in the compiled program, so there is no run-time cost.
Program 2.7: Compile-time factorial by
template recursion, and the modern constexpr
alternative; a generic algorithm; an STL algorithm.
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;
/* Compile-time computation with a recursive class
template */
template <int N>
struct Factorial
{
static
const int value = N * Factorial<N - 1>::value;
};
template <>
struct Factorial<0>
{
static
const int value = 1;
};
/* Modern alternative: a constexpr function (C++11
and later) */
constexpr int factorial(int n)
{
return (n
<= 1) ? 1 : n * factorial(n - 1);
}
/* One generic algorithm works for every type that
supports operator< */
template <typename T>
T smallest(const vector<T> &v)
{
return
*min_element(v.begin(), v.end());
}
int main()
{
cout
<< "5! (template) = "
<< Factorial<5>::value << endl;
constexpr
int f6 = factorial(6);
static_assert(f6 == 720, "6! must be 720");
cout
<< "6! (constexpr) = " << f6 << endl;
vector<int> a = {42, 7, 19};
vector<string> s = {"pear", "apple",
"mango"};
cout
<< smallest(a) << endl;
cout
<< smallest(s) << endl;
sort(a.begin(), a.end());
// STL algorithm: also a template
for (int x
: a) cout << x << " ";
cout
<< endl;
return 0;
}
Output:
5! (template)
= 120
6! (constexpr) = 720
7
apple
7 19 42
•
Factorial<5>::value
is computed by the compiler: Factorial<5>
instantiates Factorial<4>, and so on
until the specialization Factorial<0>
stops the recursion.
•
static_assert
checks a condition at compile time; the program does not compile if it is
false.
•
smallest()
works for vector<int> and vector<string> without any change.
|
Version Note: C++11:
constexpr
functions (such as factorial above) and variadic
templates (template <typename... Args>) are added.
For compile-time calculations constexpr is now
preferred to recursive templates. C++14:
variable templates and relaxed constexpr. C++17:
if
constexpr, fold expressions and class template argument
deduction. C++20: concepts and constraints, and modules. |
7.4 Summary: what templates make possible
|
Capability |
Example |
|
Generic functions |
maximum, swapValues, std::sort |
|
Generic containers |
Stack<T>, std::vector<T>, std::map<K,
V> |
|
Specialization |
A special version for bool or for pointers |
|
Compile-time computation |
Factorial<5>::value, constexpr |
|
Type safety without run-time cost |
Errors found by the compiler; no virtual calls needed |
|
Generic programming style |
Algorithms written once for many types, the basis of the
STL |
8. Function Templates versus
Class Templates
|
Feature |
Function
template |
Class
template |
|
Purpose |
Generic function |
Generic class |
|
Syntax |
template
<typename T>
before the function |
template
<typename T>
before the class |
|
Type arguments |
Usually deduced from the call; may be explicit |
Written explicitly: Box<int> (deduced only from C++17 on) |
|
Partial specialization |
Not allowed |
Allowed |
|
Typical examples |
maximum, swap, sort |
Stack, Queue, Pair, vector |
9. Rules and Common Mistakes
|
Mistake |
Correction |
|
Writing Box b; with no type argument (before
C++17) |
Write Box<int>
b; |
|
Defining out-of-class members without the template header |
Repeat template
<typename T> and
use ClassName<T>:: |
|
Putting the definition of a template in a .cpp file |
Define templates in headers (export
no longer exists) |
|
Forgetting typename before a dependent type |
Write typename
Container::value_type |
|
Using a variable as a non-type argument |
Use a constant expression such as 5 or a constexpr value |
|
Assuming Box<int> and Box<double> are related |
They are unrelated types; convert values explicitly |
|
Using a type that lacks an operator needed by the template |
Supply the operator, or constrain the template (C++20
concepts) |
10. Quick Revision
•
Class template = class with type parameters; Box<int> is an instantiation. Out-of-class members
need the template header and ClassName<T>::.
•
Several parameters are allowed; non-type parameters and
default arguments are allowed (template
<typename T, int SIZE = 5>).
•
typename:
declares type parameters, and marks dependent type names such as typename C::value_type.
•
export
template (C++98) was removed in C++11; in C++20 export
is used for modules.
•
Specialization (full and partial for classes)
customises behaviour; compile-time computation is possible with templates and constexpr.
11. Important Examination
Questions
Short-answer questions
1.
What is a generic class? Write its syntax.
2.
How is a member function of a class template defined
outside the class?
3.
What is a non-type template parameter? What is a
default template argument?
4.
What is the use of the typename
keyword? Give an example of a dependent name.
5.
What was the purpose of the export
keyword? Why was it removed?
6.
What is template specialization? Differentiate between
full and partial specialization.
7.
What is template metaprogramming?
Long-answer and
programming questions
1.
Explain class templates with a program for a generic
stack.
2.
Write a class template Pair<T1,
T2> with a constructor and a display function.
3.
Write a program using a class template with a non-type
parameter.
4.
Explain the typename
and export keywords with examples and
mention how they differ between C++ versions.
5.
Explain the power of templates with the STL,
specialization and compile-time computation.
6.
Differentiate between function templates and class
templates.
No comments:
Post a Comment