16-Templates and Generic Programming

本文深入探讨了C++中的模板和泛型编程,包括函数模板、类模板、模板参数、类型推断、模板特化等核心概念。通过各种示例和练习,讲解如何定义和使用模板,以及如何在不同情况下选择合适的模板技术。强调了编写类型独立的代码,以提高代码的通用性和效率,并探讨了模板编译过程和错误处理。
摘要由CSDN通过智能技术生成

Please indicate the source: http://blog.csdn.net/gaoxiangnumber1

Welcome to my github: https://github.com/gaoxiangnumber1

  • Both object-oriented programming and generic programming deal with types that are not known when the program is written: OOP deals with types that are not known until run time, in GP the types become known during compilation.
  • A template is a formula for creating classes or functions. When we use a generic type, we supply the information needed to transform that formula into a specific class or function. That transformation happens during compilation.

16.1. Defining a Template

16.1.1. Function Templates

  • A function template is a formula from which we can generate type-specific versions of that function.
  • A template definition starts with template followed by a template parameter list, which is a comma-separated list of one or more template parameters bracketed by < and >. The template parameter list cannot be empty.
template<typename T>
int compare(const T &v1, const T &v2)
{
    return ((v1 > v2) ? 1 : ((v1 == v2) ? 0 : -1));
}

Instantiating a Function Template

  • When we call a function template, the compiler uses the arguments of the call to deduce the template arguments for us. The compiler uses the deduced template parameters to instantiate a specific version of the function for us.
// instantiates int compare(const vector<int>&, const vector<int>&)
vector<int> vec1{
  1, 2, 3}, vec2{
  4, 5, 6};
cout << compare(vec1, vec2) << endl;    // T is vector<int>
  • The compiler will generate a version of compare with T replaced by vector. The compiler-generated functions are referred to as an instantiation of the template.

Template Type Parameters

  • A type parameter can be used to name the return type or a function parameter type, and for variable declarations or casts inside the function body.
template <typename T>
T foo(T* p)
{
    T tmp = *p; // tmp will have the type to which p points
    // ...
    return tmp;
}
  • Each type parameter must be preceded by the keyword class or typename. Both keywords have the same meaning and can be used interchangeably inside a template parameter list. A template parameter list can use both keywords:
template <typename T, class U>
T calc(const T&, const U&);

Nontype Template Parameters

  • Nontype parameters are specified by using a specific type name and they represents a value rather than a type.
  • When the template is instantiated, nontype parameters are replaced with a value supplied by the user or deduced by the compiler. These values must be constant expressions(§2.4.4) which allows the compiler to instantiate the templates during compile time.
  • We can write compare that handles string literals that are arrays of const char. Because we cannot copy an array, we define our parameters as references to an array(6.2.4). Because we want to compare literals of different lengths, we give our template two nontype parameters: the first represent the size of the first array, the second represent the size of the second array:
template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
    return strcmp(p1, p2);
}
  • compare("hi", "mom");
    The compiler use the size of the literals to instantiate a version of the template with the sizes substituted for N and M. Since the compiler inserts a null terminator at the end of a string literal(2.1.3), the compiler will instantiate
    int compare(const char(&p1)[3], const char(&p2)[4]);
  • A nontype parameter may be an integral type, a pointer or (lvalue) reference to an object or to a function type.
    1. Arguments bound to a nontype integral parameter must be a constant expression.
    2. Arguments bound to a pointer or reference nontype parameter must have static lifetime, can’t use an nonstatic local object or a dynamic object. A pointer parameter can be instantiated by nullptr or a zero-valued constant expression.
  • A template nontype parameter is a constant value inside the template definition and it can be used when constant expressions are required, e.g., to specify the size of an array.

inline and constexpr Function Templates

  • The inline or constexpr specifier follows the template parameter list and precedes the return type:
// ok: inline specifier follows the template parameter list
template <typename T>
inline T min(const T&, const T&);

Writing Type-Independent Code

  • Two principles for writing generic code:
    1. The function parameters in the template are references to const. Ensure our function can be used on types that cannot be copied(unique_ptr and the IO types). If called with large objects, this design make the function run faster.
    2. The tests in the body use only < comparisons. Reduce the requirements on types that can be used with our function. Those types must support <, but they need not support >.
  • If we were concerned about type independence and portability, we should define function using the less(14.8.2). The problem with original version is that if a user calls it with two pointers and those pointers do not point to the same array, then the result is undefined.
// version of compare that will be correct even if used on pointers; see §14.8.2
template <typename T> int compare(const T &v1, const T &v2)
{
    if (less<T>()(v1, v2)) return -1;
    if (less<T>()(v2, v1)) return 1;
    return 0;
}

Template Compilation

  • When the compiler sees the definition of a template, it does not generate code. It generates code only when we instantiate a specific instance of the template.
  • When we call a function, the compiler needs to see only a declaration for the function. When we use objects of class type, the class definition must be available, but the definitions of the member functions need not be present.
    So, we put class definitions and function declarations in header files and definitions of ordinary and class-member functions in source files.
  • For templates, the compiler needs to have the code that defines a function template or class template member function to generate an instantiation. So, headers for templates should include definitions and declarations.

Key Concept: Templates and Headers

  • Templates contain two kinds of names:
    1. Those that do not depend on a template parameter.
    2. Those that do depend on a template parameter.
  • The template provider must ensure:
    1. All names that do not depend on a template parameter are visible when the template is used.
    2. The definition of the template, including the definitions of the members of a class template, are visible when the template is used.
  • The template user must ensure that declarations for all functions, types, and operators associated with the types used to instantiate the template are visible.
    1. Authors of templates should provide a header that contains the template definition along with declarations for all the names used in the class template or in the definitions of its members.
    2. Users of the template must include the header for the template and for any types used to instantiate that template.

Compilation Errors Are Mostly Reported during Instantiation

  • There are three stages during which the compiler might flag an error.
    1. The first stage is when we compile the template itself. The compiler can detect syntax errors but not much else.
    2. The second stage is when the compiler sees a use of the template.
      -1- For a function template, the compiler checks that the number of the arguments is appropriate and whether two arguments that are supposed to have the same type do so.
      -2- For a class template, the compiler checks that the right number of template arguments are provided but not much more.
    3. The third stage when errors are detected is during instantiation. It is only then that type-related errors can be found. Depending on how the compiler manages instantiation, these errors may be reported at link time.
  • The caller should guarantee that the arguments passed to the template support any operations that template uses, and that those operations behave correctly in the context in which the template uses them.
if (v1 < v2) return -1;     // requires < on objects of type T
if (v2 < v1) return 1;      // requires < on objects of type T
return 0;                   // returns int; not dependent on T
  • The argument type has a < operator. When the compiler processes the body of this template, it cannot verify whether the conditions in the if statements are legal. If the arguments passed to compare have a < operation, then the code is fine, but not otherwise. For example,
Sales_data data1, data2;
cout << compare(data1, data2) << endl; // error: no < on Sales_data
  • This instantiation generates a version of the function that will not compile. Errors such as this one cannot be detected until the compiler instantiates the definition of compare on type Sales_data.

Exercises Section 16.1.1

Exercise 16.1

Define instantiation.

  • The compiler-generated functions are referred to as an instantiation of the template.

Exercise 16.2

Write and test your own versions of the compare functions.

#include <iostream>

using namespace std;

template<typename T>
int compare(const T &v1, const T &v2)
{
    if(less<T>()(v1, v2))
    {
        return -1;
    }
    if(less<T>()(v2, v1))
    {
        return 1;
    }
    return 0;
}

int main()
{
    cout << compare(1, 2); // -1

    return 0;
}

Exercise 16.3

Call your compare function on two Sales_data objects to see how your compiler handles errors during instantiation.

  • error: no match for ‘operator<’ (operand types are ‘const Sales_data’ and ‘const Sales_data’)

Exercise 16.4

Write a template that acts like the library find algorithm. The function will need two template type parameters, one to represent the functions iterator parameters and the other for the type of the value. Use your function to find a given value in a vector and in a list.

#include <iostream>
#include <vector>
#include <list>
#include <string>

using namespace std;

template<typename Iterator, typename Value>
Iterator Find(Iterator first, Iterator last, const Value& value)
{
    for(; first != last && *first != value; ++first);
    return first;
}

int main()
{
    vector<int> v = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    bool is_in_vector = (v.cend() != Find(v.cbegin(), v.cend(), 5));
    cout << (is_in_vector ? "found\n" : "not found\n");

    list<string> l = { "aa", "bb", "cc", "dd", "ee", "ff", "gg" };
    bool is_in_list = (l.cend() != Find(l.cbegin(), l.cend(), "zz"));
    cout << (is_in_list ? "found\n" : "not found\n");

    return 0;
}

Exercise 16.5

Write a template version of the print function from 6.2.4(p. 217) that takes a reference to an array and can handle arrays of any size and any element type.

#include <iostream>
#include <string>

using namespace std;

template<typename T, unsigned int N>
void Print(const T (&arr)[N])
{
    for(int index = 0; index < N; ++index)
    {
        cout << arr[index] << ' ';
    }
    cout << '\n';
}

int main()
{
    int arr1[] = {
  1, 2, 3, 4, 5, 6, 7};
    string arr2[] = { "gao", "xiang", "number", "one" };
    Print(arr1);
    Print(arr2);

    return 0;
}

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.

#include <iostream>
#include <string>

using namespace std;

template<typename T, unsigned int N>
T* Begin(T (&arr)[N])
{
    return arr;
}

template<typename T, unsigned int N>
T* End(T (&arr)[N])
{
    return arr + N;
}

int main()
{
    int arr1[] = {
  1, 2, 3, 4, 5, 6, 7};
    cout << *Begin(arr1) << ' ' << *(End(arr1) - 1) << '\n';
    string arr2[] = { "gao", "xiang", "number", "one" };
    cout << *Begin(arr2) << ' ' << *(End(arr2) - 1) << '\n';

    return 0;
}

Exercise 16.7

Write a constexpr template that returns the size of a given array.

#include <iostream>
#include <string>

using namespace std;

template<typename T, unsigned int N>
constexpr int Size(T (&arr)[N])
{
    return N;
}

int main()
{
    int arr1[] = {
  1, 2, 3, 4, 5, 6, 7};
    string arr2[] = { "gao", "xiang", "number", "one" };
    cout << Size(arr1) << ' ' << Size(arr2) << '\n';

    return 0;
}

Exercise 16.8

In the Key Concept box on page 108, we noted that as a matter of habit C++ programmers prefer using != to using <. Explain the rationale for this habit.

  • Because more class defines “!=” rather than “<”. Doing so can reduce the requirements of the class used with a template class.

16.1.2. Class Templates

  • Since the compiler cannot deduce the template parameter type(s) for a class template, we must supply the list of template arguments to use in place of the template parameters inside angle brackets following the templates name(3.3).

Defining a Class Template

  • Class templates begin with template followed by a template parameter list, we use the template parameters as stand-ins for types or values that will be supplied when the template is used.
template <typename T>
class Blob
{
public:
    typedef T value_type;
    typedef typename std::vector<T>::size_type size_type;

    // constructors
    Blob();
    Blob(std::initializer_list<T> 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
    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
private:
    std::shared_ptr<std::vector<T>> data;
    // throws msg if data[i] isn't valid
    void check(size_type i, const std::string &msg) const;
};

Instantiating a Class Template

  • When we use a class template, we must supply a list of explicit template arguments that are bound to the templates parameters. The compiler uses these template arguments to instantiate a specific class from the template.
Blob<int> ia;                   // empty Blob<int>
Blob<int> ia2 = {
  0, 1, 2, 3, 4};    // Blob<int> with five elements
  • When the compiler instantiates a class from Blob template, it rewrites the template by replacing each instance of the template parameter T with the given template argument(int). The compiler will instantiate a class that is equivalent to
template <>
class Blob<int>
{
    typedef typename std::vector<int>::size_type size_type;
    //...
};
  • The compiler generates a different class for each element type we specify.
// these definitions instantiate two distinct Blob types
Blob<string> names;     // Blob that holds strings
Blob<double> prices;        // different element type

References to a Template Type in the Scope of the Template

  • The name of a class template is not the name of a type(3.3). A class template is used to instantiate a type, and an instantiated type always includes template argument(s).

Member Functions of Class Templates

  • As with any class, we can define the member functions of a class template either inside or outside of the class body; members defined inside the class body are implicitly inline.
  • Each instantiation of the class template has its own version of each member. A member function defined outside the class template body starts with template followed by the class template parameter list. For member function of Blob:
template <typename T>
return_type Blob<T>::member_name(parameter_list)

The check and Element Access Members

template <typename T>
void Blob<T>::check(size_type i, const std::string &msg) const
{
    if(i >= data->size())
        throw std::out_of_range(msg);
}
  • The subscript operator and back function use the template parameter to specify the return type.
template <typename T>
T& Blob<T>::back()
{
    check(0, "back on empty Blob");
    return data->back();
}
template <typename T>
T& Blob<T>::operator[](size_type i)
{
    // if i is too big, check will throw, preventing access to a nonexistent element
    check(i, "subscript out of range");
    return (*data)[i];
}
template <typename T>
void Blob<T>::pop_back()
{
    check(0, "pop_back on empty Blob");
    data->pop_back();
}

Blob Constructors

template <typename T>
Blob<T>::Blob(): data(std::make_shared<std::vector<T>>()) {}
template <typename T>
Blob<T>::Blob(std::initializer_list<T> il): data(std::make_shared<std::vector<T>>(il)) {}

Instantiation of Class-Template Member Functions

  • By default, a member function of a class template is instantiated only if the program uses that member function. Following codes instantiates the Blob class and three member functions: operator[], size, and the initializer_list constructor.
// instantiates Blob<int> and the initializer_list<int> constructor
Blob<int> squares = {
  0,1,2,3,4,5,6,7,8,9};
// instantiates Blob<int>::size() const
for (size_t i = 0; i != squares.size(); ++i)
{
    squares[i] = i*i; // instantiates Blob<int>::operator[](size_t)
}
  • The fact that members are instantiated only if we use them lets us instantiate a class with a type that may not meet the requirements for some of the templates operations.

Simplifying Use of a Template Class Name inside Class Code

  • One exception to the rule “Must supply template arguments when using a class template type”: inside the scope of the class template itself, we may use the name of the template without arguments:
// BlobPtr throws an exception on attempts to access a nonexistent element
template <typename T>
class BlobPtr
{
public:
    BlobPtr(): curr(0) {}
    BlobPtr(Blob<T> &a, size_t sz = 0): wptr(a.data), curr(sz) {}
    T& operator*() const
    {
        auto p = check(curr, "dereference past end");
        return (*p)[curr]; // (*p) is the vector to which this object points
    }
    // increment and decrement
    BlobPtr& operator++(); // prefix operators
    BlobPtr& operator--();

private:
    // check returns a shared_ptr to the vector if the check succeeds
    std::shared_ptr<std::vector<T>> check(std::size_t, const std::string&) const;
    // store a weak_ptr, which means the underlying vector might be destroyed
    std::weak_ptr<std::vector<T>> wptr;
    std::size_t curr; // current position within the array
};
  • The prefix increment and decrement members of BlobPtr return BlobPtr&, not BlobPtr&. When we are inside the scope of a class template, the compiler treats references to the template itself as if we had supplied template arguments matching the templates own parameters.

Using a Class Template Name outside the Class Template Body

  • When we define members outside the body of a class template, we are not in the scope of the class until the class name is seen(7.4):
// postfix: increment/decrement the object but return the unchanged value
template <typename T>
BlobPtr<T> BlobPtr<T>::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
}
    1. Because the return type appears outside the scope of the class, we must specify that the return type returns a BlobPtr instantiated with the same type as the class.
    2. Inside the function body, we are in the scope of the class, so don’t need to repeat the template argument when we define ret. When we do not supply template arguments, the compiler assumes that we are using the same type as the members instantiation.

Class Templates and Friends

  • If a class contains a friend declaration(7.2.1), the class and the friend can independently be templates or not. When the class is a template:
    1. Friend is a nontemplate: friend access to all the instantiations of the class template.
    2. Friend is a template: the class granting friendship controls whether friendship includes all instantiations of the template or only specific instantiation(s).

One-to-One Friendship

  • The most common form of friendship from a class template to another template(class or function) establishes friendship between corresponding instantiations of the class and its friend. For example, Blob class should declare the BlobPtr class and a template version of the Blob equality operator(originally defined for StrBlob in the exercises in 14.3.1(p. 562)) as friends.
  • In order to refer to a specific instantiation of a template(class or function), we must first declare the template itself. A template declaration includes the template parameter list.
// forward declarations needed for friend declarations in Blob
template <typename> class BlobPtr;
template <typename> class Blob; // needed for parameters in operator==
template <typename T>
bool operator==
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值