Introduction
Both object-oriented programming (OOP) and generic programming deal with types that are not known at the time the program is written. The distinction between the two is that OOP deals with types that are not known until run time, whereas in generic programming the types become known during compilation.
we write the code in a way that is independent of any particular type. When we use a generic program, we supply the type(s) or value(s) on which that instance of the program will operate.
Templates are the foundation for generic programming in C++. A template is a blueprint or formula for creating classes or functions. When we use a generic type,such as vector, or a generic function, such as find, we supply the information needed to transform that blueprint into a specific class or function. That transformation happens during compilation.
template
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
A template definition starts with the keyword template followed by a templateparameter list, which is a comma-separated list of one or more template parameters bracketed by the less-than (<) and greater-than (>) tokens.
template
int compare(const char (&p1)[N], const char (&p2)[M])
{
return strcmp(p1, p2);
}
template
InputIt find(InputIt first, InputIt last, const T& value)
{
for (; first != last; ++first) {
if (*first == value) {
return first;
}
}
return last;
}
template// N must be constexpr
unsigned print(T (&arr)[N])
{
//by using array index to print
for(int i = 0;i
Exercise 16.6: How do you think the library begin and end functions that take an array argument work? Define your own versions of these functions.
//the same as std::begin
template
T* begin_def(T (&arr)[size])
{
return arr;
}
// the same as std::end
//this should not be const
template
T* end_def(T (&arr)[size])
{
return arr+size;
}
template class Blob {
public:
typedef T value_type;
typedef typename std::vector::size_type size_type;
// constructors
Blob();
Blob(std::initializer_list il);
// number of elements in the Blob
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
// add and remove elements
void push_back(const T &t) {data->push_back(t);}
// move version; see § 13.6.3 (p. 548)
void push_back(T &&t) { data->push_back(std::move(t)); }
void pop_back();
// element access
T& back();
T& operator[](size_type i); // defined in § 14.5 (p. 566)
private:
std::shared_ptr> data;
// throws msg if data[i] isn't valid
void check(size_type i, const std::string &msg) const;
};
Blob ia; // empty Blob
Blob ia2 = {0,1,2,3,4}; // Blob with five elements
std::shared_ptr> data;
shared_ptr>
ret-type StrBlob::member-name(parm-list)
the corresponding Blob member will look like
template
ret-type Blob::member-name(parm-list)
template
BlobPtr BlobPtr::operator++(int)
{
// no check needed here; the call to prefix increment will do the check
BlobPtr ret = *this; // save the current value
++*this; // advance one element; prefix ++ checks the increment
return ret; // return the saved state
}
// forward declarations needed for friend declarations in Blob
template class BlobPtr;
template class Blob; // needed for parameters in operator==
template
bool operator==(const Blob&, const Blob&);
template class Blob {
// each instantiation of Blob grants access to the version of
// BlobPtr and the equality operator instantiated with the same type
friend class BlobPtr;
friend bool operator==
(const Blob&, const Blob&);
// other members as in § 12.1.1 (p. 456)
};
Blob ca; // BlobPtr and operator== are friends
Blob ia; // BlobPtr and operator== are friends
// forward declaration necessary to befriend a specific instantiation of a template
template class Pal;
class C { // C is an ordinary, nontemplate class
friend class Pal; // Pal instantiated with class C is a friend to
C
// all instances of Pal2 are friends to C;
// no forward declaration required when we befriend all instantiations
template friend class Pal2;
};
template class C2 { // C2 is itself a class template
// each instantiation of C2 has the same instance of Pal as a friend
friend class Pal; // a template declaration for Pal must be in
scope
// all instances of Pal2 are friends of each instance of C2, prior declaration
needed
template friend class Pal2;
// Pal3 is a nontemplate class that is a friend of every instance of C2
friend class Pal3; // prior declaration for Pal3 not needed
};
template using twin = pair;
twin authors; // authors is a pair
template using partNo = pair;
partNo books; // books is a pair
partNo cars; // cars is a pair
partNo kids; // kids is a pair
template class Foo {
public:
static std::size_t count() { return ctr; }
// other interface members
private:
static std::size_t ctr;
// other implementation members
};
// instantiates static members Foo::ctr and Foo::count
Foo fs;
// all three objects share the same Foo::ctr and Foo::count members
Foo fi, fi2, fi3;
template
size_t Foo::ctr = 0; // define and initialize ctr
Foo fi; // instantiates Foo class
// and the static data member ctr
auto ct = Foo::count(); // instantiates Foo::count
ct = fi.count(); // uses Foo::count
ct = Foo::count(); // error: which template instantiation?
Like any other member function, a static member function is instantiated only if it istypedef double A;
template void f(A a, B b)
{
A tmp = a; // tmp has same type as the template parameter A, not double
double B; // error: redeclares template parameter B
}
// error: illegal reuse of template parameter name V
template // ...
// all three uses of calc refer to the same function template
template T calc(const T&, const T&); // declaration
template U calc(const U&, const U&); // declaration
// definition of the template
template
Type calc(const Type& a, const Type& b) { /* . . . */ }
template
typename T::value_type top(const T& c)
{
if (!c.empty())
return c.back();
else
return typename T::value_type();
}
Default Template Arguments
Whenever we use a class template, we must always follow the template’s name with brackets. The brackets indicate that a class must be instantiated from a template. In particular, if a class template provides default arguments for all of its template parameters,and we want to use those defaults, we must put an empty bracket pair following the template’s name:
template class Numbers { // by default T is int
public:
Numbers(T v = 0): val(v) { }
// various operations on numbers
private:
T val;
};
Numbers lots_of_precision;
Numbers<> average_precision; // empty <> says we want the default type
// Exercise 16.17:
// What, if any, are the differences between a type parameter that is declared
// as a typename and one that is declared as a class? When must typename be used?
//
// There is no difference. typename and class are interchangeable in the
// declaration of a type template parameter.
// You do, however, have to use class (and not typename) when declaring a
// template template parameter.
//
// When we want to inform the compiler that a name represents a type, we must use
// the keyword typename, not class
//
// Exercise 16.19:
// Write a function that takes a reference to a container and prints the
// elements in that container. Use the container’s size_type and size members
// to control the loop that prints the elements.
//
// Exercise 16.20:
// Rewrite the function from the previous exercise to use iterators returned
// from begin and end to control the loop.
//
#include
#include
#include
// ex16.19
template
std::ostream& print(Container const& container, std::ostream& os)
{
for(typename Container::size_type i = 0; i != container.size(); ++ i)
os << container[i] << " ";
return os;
}
// ex16.20
template
std::ostream& print2(Container const& container, std::ostream &os)
{
for(auto curr = container.cbegin(); curr != container.cend(); ++curr)
os << *curr << " ";
return os;
}
int main()
{
std::vector v = { 1, 23, 6, 4, 5, 7, 4 };
std::list l = { "ss", "sszz", "saaas", "s333s", "ss2"," sss" };
print2(v, std::cout) << std::endl;
print2(l, std::cout) << std::endl;
return 0;
}
// function-object class that calls delete on a given pointer
class DebugDelete {
public:
DebugDelete(std::ostream &s = std::cerr): os(s) { }
// as with any function template, the type of T is deduced by the compiler
template void operator()(T *p) const
{ os << "deleting unique_ptr" << std::endl; delete p;
}
private:
std::ostream &os;
};
double* p = new double;
DebugDelete d; // an object that can act like a delete expression
d(p); // calls DebugDelete::operator()(double*), which deletes p
int* ip = new int;
// calls operator()(int*) on a temporary DebugDelete object
DebugDelete()(ip);
// destroying the the object to which p points
// instantiates DebugDelete::operator()(int *)
unique_ptr p(new int, DebugDelete());
// destroying the the object to which sp points
// instantiates DebugDelete::operator()(string*)
unique_ptr sp(new string,
DebugDelete());
The unique_ptr destructor calls the DebugDelete’s call operator. Thus, whenever unique_ptr’s destructor is instantiated, DebugDelete’s call operator will also be instantiated: Thus, the definitions above will instantiate:
// sample instantiations for member templates of DebugDelete
void DebugDelete::operator()(int *p) const { delete p; }
void DebugDelete::operator()(string *p) const { delete p; }
template class Blob {
template Blob(It b, It e);
// ...
};
Instantiation and Member Templates
int ia[] = {0,1,2,3,4,5,6,7,8,9};
C++ Primer, Fifth Edition
vector vi = {0,1,2,3,4,5,6,7,8,9};
list w = {"now", "is", "the", "time"};
// instantiates the Blob class
// and the Blob constructor that has two int* parameters
Blob a1(begin(ia), end(ia));
// instantiates the Blob constructor that has
// two vector::iterator parameters
Blob a2(vi.begin(), vi.end());
// instantiates the Blob class and the Blob
// constructor that has two (list::iterator parameters
Blob a3(w.begin(), w.end());
The fact that instantiations are generated when a template is used (§ 16.1.1, p. 656) means that the same instantiation may appear in multiple object files. When two or more separately compiled source files use the same template with the same template arguments, there is an instantiation of that template in each of those files。
extern template declaration; // instantiation declaration
template declaration; // instantiation definition
// instantion declaration and definition
extern template class Blob; // declaration
template int compare(const int&, const int&); // definition
When the compiler sees an extern template declaration, it will not generate code for that instantiation in that file. Declaring an instantiation as extern is a promise that there will be a nonextern use of that instantiation elsewhere in the program There may be several extern declarations for a given instantiation but there must be exactly one definition for that instantiation.
// Application.cc
// these template types must be instantiated elsewhere in the program
extern template class Blob;
extern template int compare(const int&, const int&);
Blob sa1, sa2; // instantiation will appear elsewhere
// Blob and its initializer_list constructor instantiated in this file
Blob a1 = {0,1,2,3,4,5,6,7,8,9};
Blob a2(a1); // copy constructor instantiated in this file
int i = compare(a1[0], a2[0]); // instantiation will appear elsewhere
The file Application.o will contain instantiations for Blobwarning: There must be an explicit instantiation definition somewhere in the program for every instantiation declaration.
Instantiation Definitions Instantiate All Members
note : An instantiation definition can be used only for types that can be used with every member function of a class template.
Binding the Deleter at Run Time
Binding the Deleter at Compile Time
By binding the deleter at compile time, unique_ptr avoids the run-time cost of an indirect call to its deleter. By binding the deleter at run time, shared_ptr makes it easier for users to override the deleter.
• const conversions: A function parameter that is a reference (or pointer) to a const can be passed a reference (or pointer) to a nonconst object (§ 4.11.2, p. 162).
• Array- or function-to-pointer conversions: If the function parameter is not a reference type, then the normal pointer conversion will be applied to arguments of array or function type. An array argument will be converted to a pointer to its first element. Similarly, a function argument will be converted to a pointer to the
function’s type (§ 4.11.2, p. 161).
Other conversions, such as the arithmetic conversions (§ 4.11.1, p. 159), derived-to-base (§ 15.2.2, p. 597), and user-defined conversions (§ 7.5.4, p. 294, and § 14.9, p.579), are not performed.
template T fobj(T, T); // arguments are copied
template T fref(const T&, const T&); // references
string s1("a value");
const string s2("another value");
fobj(s1, s2); // calls fobj(string, string); const is ignored
fref(s1, s2); // calls fref(const string&, const string&)
// uses premissible conversion to const on s1
int a[10], b[42];
fobj(a, b); // calls f(int*, int*)
fref(a, b); // error: array types don't matc
Note:const conversions and array or function to pointer are the only automatic conversions for arguments to parameters with template types.Note:Normal conversions are applied to arguments whose type is not a template parameter.
// Exercise 16.32:
// What happens during template argument deduction?
// The process of determining the template arguments from the function arguments
// is known as template argument deduction. During template argument deduction,
// the compiler uses types of the arguments in the call to find the template
// arguments that generate a version of the function that best matches the given
// call.
// T1 cannot be deduced: it doesn't appear in the function parameter list
template
T1 sum(T2, T3);
// T1 is explicitly specified; T2 and T3 are inferred from the argument types
auto val3 = sum(i, lng); // long long sum(int, long)
// poor design: users must explicitly specify all three template parameters
template
T3 alternative_sum(T2, T1);
// a trailing return lets us declare the return type after the parameter list is seen
template
auto fcn(It beg, It end) -> decltype(*beg)
{
// process the range
return *beg; // return a reference to an element from the range
}
return type is object: ( we can use remove_reference to obtain the element type. The remove_reference template has one template type parameter and a (public) type member named type. If we instantiate remove_reference with a reference type, then type will be the referred-to type. For example, if we instantiate remove_referencetemplate
auto fcn2(It beg, It end) ->
typename remove_reference::type
{
// process the range
return *beg; // return a copy of an element from the range
}
Exercise 16.41: Write a version of sum with a return type that is guaranteed to be large enough to hold the result of the addition.
template
auto sum(T lhs, T rhs) -> decltype( lhs + rhs)
{
return lhs + rhs;
}
must be such that it allows a unique type or value to be determined for each
template parameter.
template void f1(T&); // argument must be an lvalue
// calls to f1 use the referred-to type of the argument as the template parameter type
f1(i); // i is an int; template parameter T is int
f1(ci); // ci is a const int; template parameter T is const int
f1(5); // error: argument to a & parameter must be an lvalue
template void f2(const T&); // can take an rvalue
// parameter in f2 is const &; const in the argument is irrelevant
// in each of these three calls, f2's function parameter is inferred as const int&
f2(i); // i is an int; template parameter T is int
f2(ci); // ci is a const int, but template parameter T is int
f2(5); // a const & parameter can be bound to an rvalue; T is int
template void f3(T&&);
f3(42); // argument is an rvalue of type int; template parameter T is int
f3(i); // argument is an lvalue; template parameter T is int&
f3(ci); // argument is an lvalue; template parameter T is const int&
// invalid code, for illustration purposes only
void f3(int& &&); // when T is int&, function parameter is int& &&
The function parameter in f3 is T&& and T is int&, so T&& is int& &&, which
collapses to int&. Thus, even though the form of the function parameter in f3 is an
rvalue reference (i.e., T&&), this call instantiates f3 with an lvalue reference type
(i.e., int&):
void f3(int&); // when T is int&, function parameter collapses to int&
template void f3(T&& val)
{
T t = val; // copy or binding a reference?
t = fcn(t); // does the assignment change only t or val and t?
if (val == t) { /* ... */ } // always true if T is a reference type
}
It is surprisingly hard to write code that is correct when the types involved might be plain (nonreference) types or reference types.
In practice, rvalue reference parameters are used in one of two contexts: Either the template is
forwarding its arguments
, or t
he template is overloaded
. We’ll look at forwarding in § 16.2.7 (p. 692) and at template overloading in § 16.3 (p. 694).
// Exercise 16.42:
// Determine the type of T and of val in each of the following calls:
// template void g(T&& val);
// int i = 0; const int ci = i;
// (a) g(i);
// since i is lvalue, T is deduced as int&, val is int& && collapsing to int&
// (b) g(ci);
// since ci is lvaue, T is deduced as const int&, val is const int& && collapsing to const int&
// (c) g(i * ci);
// since i * ci is rvalue, T is deduced as int, val is int&& && colapsing to int&&
Exercise 16.44:
// Using the same three calls as in the first exercise, determine the types for T
// if g’s function parameter is declared as T (not T&&).
// ^
// g(i); -- T is deduced as int
// g(ci); -- T is deduced as int, const is ignored.
// g(i * ci); -- T is deduced as int, (i * ci) returns rvalue which is copied to
// T
// What if g’s function parameter is const T&?
// ^^^^^^^^
// g(i) -- T is deduced as int , val : const int&
// g(ci) -- T is deduced as int , val : const int&
// g(i * ci) -- T is deduced as int , val : const int&(see example on page 687)
//
// for the use of typename in the return type and the cast see § 16.1.3 (p. 670)
// remove_reference is covered in § 16.2.3 (p. 684)
template
typename remove_reference::type&& move(T&& t)
{
// static_cast covered in § 4.11.3 (p. 163)
return static_cast::type&&>(t);
}
template
void flip(F f,T1&& t1,T2&& t2)
{
f(std::forward(t2),std::forward(t1));
}
//
// Exercise 16.48:
// Write your own versions of the debug_rep functions.
//
#include
#include
#include
// always declare first:
template std::string debug_rep(const T& t);
template std::string debug_rep(T* p);
std::string debug_rep(const std::string &s);
std::string debug_rep(char* p);
std::string debug_rep(const char *p);
// print any type we don't otherwise.
template std::string debug_rep(const T& t)
{
std::ostringstream ret;
ret << t;
return ret.str();
}
// print pointers as their pointer value, followed by the object to which the pointer points
template std::string debug_rep(T* p)
{
std::ostringstream ret;
ret << "pointer: " << p;
if(p)
ret << " " << debug_rep(*p);
else
ret << " null pointer";
return ret.str();
}
// non-template version
std::string debug_rep(const std::string &s)
{
return '"' + s + '"';
}
// convert the character pointers to string and call the string version of debug_rep
std::string debug_rep(char *p)
{
return debug_rep(std::string(p));
}
std::string debug_rep(const char *p)
{
return debug_rep(std::string(p));
}
int main()
{
}
// Exercise 16.49:
// Explain what happens in each of the following calls:
//
// Exercise 16.50:
// Define the functions from the previous exercise so that they print an
// identifying message. Run the code from that exercise. If the calls behave
// differently from what you expected, make sure you understand why.
//
#include
#include
#include
template void f(T)
{
std::cout << "f(T)\n";
}
template void f(const T*)
{
std::cout << "f(const T*)\n";
}
template void g(T)
{
std::cout << "template void g(T)\n";
}
template void g(T*)
{
std::cout << "template void g(T*)\n";
}
int main()
{
int i = 42, *p = &i;
const int ci = 0, *p2 = &ci;
//g(42); //template void g(T ); --is called
//g(p); //template void g(T*); --is called
//g(ci); //template void g(T) --is called
//g(p2); //template void g(T*) --is called
//f(42); //f(T)
//f(p); //f(T)
//f(ci); //f(T)
f(p2); //f(const T*)
}
// Args is a template parameter pack; rest is a function parameter pack
// Args represents zero or more template type parameters
// rest represents zero or more function parameters
template
void foo(const T &t, const Args& ... rest);
// function to end the recursion and print the last element
// this function must be declared before the variadic version of print is defined
template
ostream &print(ostream &os, const T &t)
{
return os << t; // no separator after the last element in the pack
}
// this version of print will be called for all but the last element in the pack
template
ostream &print(ostream &os, const T &t, const Args&... rest)
{
os << t << ", "; // print the first argument
return print(os, rest...); // recursive call; print the other arguments
}
// call debug_rep on each argument in the call to print
template
ostream &errorMsg(ostream &os, const Args&... rest)
{
// print(os, debug_rep(a1), debug_rep(a2), ..., debug_rep(an)
return print(os, debug_rep(rest)...);
}
// fun has zero or more parameters each of which is
// an rvalue reference to a template parameter type
template
void fun(Args&&... args) // expands Args as a list of rvalue references
{
// the argument to work expands both Args and args
work(std::forward(args)...);
}
Here we want to forward all of fun’s arguments to another function named work that presumably does the real work of the function.