zoukankan      html  css  js  c++  java
  • C++ 模板与泛型编程

    《C++ Primer 4th》读书笔记

    所谓泛型编程就是以独立于任何特定类型的方式编写代码。泛型编程与面向对象编程一样,都依赖于某种形式的多态性。

    面向对象编程中的多态性在运行时应用于存在继承关系的类。我们能够编写使用这些类的代码,忽略基类与派生类之间类型上的差异。

    在泛型编程中,我们所编写的类和函数能够多态地用于跨越编译时不相关的类型。一个类或一个函数可以用来操纵多种类型的对象。

    面向对象编程所依赖的多态性称为运行时多态性,泛型编程所依赖的多态性称为编译时多态性或参数式多态性。

     

    模板是泛型编程的基础。模板是创建类或函数的蓝图或公式。

     

    函数模板

    模板定义以关键字 template 开始,后接模板形参表,模板形参表是用尖括号括住的一个或多个模板形参的列表,形参之间以逗号分隔。模板形参表不能为空。

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

    模板形参可以是表示类型的类型形参,也可以是表示常量表达式的非类型形参。类型形参跟在关键字 class 或 typename 之后定义.在函数模板形参表中,关键字 typename 和 class 具有相同含义,可以互换使用,两个关键字都可以在同一模板形参表中使用:

    // ok: no distinction between typename and class in template parameter list
    
    template <typename T, class U> calc (const T&, const U&);

    模板形参表示可以在类或函数的定义中使用的类型或值。使用函数模板时,编译器会推断哪个(或哪些)模板实参绑定到模板形参。一旦编译器确定了实际的模板实参,就称它实例化了函数模板的一个实例。

    实质上,编译器将确定用什么类型代替每个类型形参,以及用什么值代替每个非类型形参。推导出实际模板实参后,编译器使用实参代替相应的模板形参产生编译该版本的函数。编译器承担了为我们使用的每种类型而编写函数的单调工作。

    int main ()
    
    {
    
    // T is int;
    
    // compiler instantiates int compare(const int&, const int&)
    
    cout << compare(1, 0) << endl;
    
    // T is string;
    
    // compiler instantiates int compare(const string&, const string&)
    
    string s1 = "hi", s2 = "world";
    
    cout << compare(s1, s2) << endl;
    
    return 0;
    
    }

     函数模板可以用与非模板函数一样的方式声明为 inline。说明符放在模板形参表之后、返回类型之前,不能放在关键字 template 之前。

    // ok: inline specifier follows template parameter list
    
    template <typename T> inline T min(const T&, const T&);
    
    // error: incorrect placement of inline specifier
    
    inline template <typename T> T min(const T&, const T&);

    类模板

    类模板也是模板,因此必须以关键字 template 开头,后接模板形参表。Queue 模板接受一个名为 Type 的模板类型形参。

    除了模板形参表外,类模板的定义看起来与任意其他类问相似。类模板可以定义数据成员、函数成员和类型成员,也可以使用访问标号控制对成员的访问,还可以定义构造函数和析构函数等等。在类和类成员的定义中,可以使用模板形参作为类型或值的占位符,在使用类时再提供那些类型或值。

    template <class Type> class Queue {
    
    public:
    
    Queue (); // default constructor
    
    Type &front (); // return element from head of Queue
    
    const Type &front () const;
    
    void push (const Type &); // add element to back of Queue
    
    void pop(); // remove element from head of Queue
    
    bool empty() const; // true if no elements in the Queue
    
    private:
    
    // ...
    
    };

    与调用函数模板形成对比,使用类模板时,必须为模板形参显式指定实参:

    Queue<int> qi; // Queue that holds ints
    
    Queue< vector<double> > qc; // Queue that holds vectors of doubles
    
    Queue<string> qs; // Queue that holds strings

    除了定义数据成员或函数成员之外,类还可以定义类型成员。如果要在函数模板内部使用这样的类型,必须告诉编译器我们正在使用的名字指的是一个类型。必须显式地这样做,因为编译器(以及程序的读者)不能通过检查得知,由类型形参定义的名字何时是一个类型何时是一个值。如果希望编译器将 size_type 当作类型,则必须显式告诉编译器这样做:

    template <class Parm, class U>
    
    Parm fcn(Parm* array, U value)
    
    {
    
    typename Parm::size_type * p; // ok: declares p to be a pointer
    
    }

    通过在成员名前加上关键字 typename 作为前缀,可以告诉编译器将成员当作类型。

    如果拿不准是否需要以 typename 指明一个名字是一个类型,那么指定它是个好主意。在类型之前指定 typename 没有害处,因此,即使 typename 是不必要的,也没有关系。

     

    非类型模板形参

    模板形参不必都是类型。模板非类型形参是模板定义内部的常量值,在需要常量表达式的时候,可使用非类型形参(例如,像这里所做的一样)指定数组的长度。

    // initialize elements of an array to zero
    
    template <class T, size_t N> void array_init(T (&parm)[N])
    
    {
    
    for (size_t i = 0; i != N; ++i) {
    
    parm[i] = 0;
    
    }
    
    }
    
    int x[42];
    
    double y[10];
    
    array_init(x); // instantiates array_init(int(&)[42]
    
    array_init(y); // instantiates array_init(double(&)[10]

    类型等价性与非类型形参: 对模板的非类型形参而言,求值结果相同的表达式将认为是等价的。array_init 调用引用的是相同的实例—— array_init<int, 42>:

    int x[42];
    
    const int sz = 40;
    
    int y[sz + 2];
    
    array_init(x); // instantiates array_init(int(&)[42])
    
    array_init(y); // equivalent instantiation

    在函数模板内部完成的操作限制了可用于实例化该函数的类型。程序员的责任是,保证用作函数实参的类型实际上支持所用的任意操作,以及保证在模板使用哪些操作的环境中那些操作运行正常。

     

    编写独立于类型的代码的一般原则:编写模板代码时,对实参类型的要求尽可能少是很有益的。说明了编写泛型代码的两个重要原则:

    • 模板的形参是 const 引用。

    • 函数体中的测试只用 < 比较。

    通过将形参设为 const 引用,就可以允许使用不允许复制的类型。大多数类型(包括内置类型和我们已使用过的除 IO 类型之外的所有标准库的类型)都允许复制。但是,也有不允许复制的类类型。将形参设为 const 引用,保证这种类型可以用于 compare 函数,而且,如果有比较大的对象调用 compare,则这个设计还可以使函数运行得更快。

     

    实例化

    模板是一个蓝图,它本身不是类或函数。编译器用模板产生指定的类或函数的特定类型版本。产生模板的特定类型实例的过程称为实例化。模板在使用时将进行实例化,类模板在引用实际模板类类型时实例化,函数模板在调用它或用它对函数指针进行初始化或赋值时实例化。

     

    类模板的每次实例化都会产生一个独立的类类型。为 int 类型实例化的 Queue 与任意其他 Queue 类型没有关系,对其他Queue 类型成员也没有特殊的访问权。

     

    从函数实参确定模板实参的类型和值的过程叫做模板实参推断。

     

    类型形参的实参的受限转换

    一般而论,不会转换实参以匹配已有的实例化,相反,会产生新的实例。除了产生新的实例化之外,编译器只会执行两种转换:

    • const 转换:接受 const 引用或 const 指针的函数可以分别用非 const对象的引用或指针来调用,无须产生新的实例化。如果函数接受非引用类型,形参类型实参都忽略 const,即,无论传递 const 或非 const 对象给接受非引用类型的函数,都使用相同的实例化。

    • 数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换。数组实参将当作指向其第一个元素的指针,函数实参当作指向函数类型的指针。

    template <typename T> T fobj(T, T); // arguments are copied
    
    template <typename T>
    
    T fref(const T&, const T&); // reference arguments
    
    string s1("a value");
    
    const string s2("another value");
    
    fobj(s1, s2); // ok: calls f(string, string), const is ignored
    
    fref(s1, s2); // ok: non const object s1 converted to const reference int a[10], b[42];
    
    fobj(a, b); // ok: calls f(int*, int*)
    
    fref(a, b); // error: array types don't match; arguments aren't converted to pointers

    模板实参推断与函数指针

    可以使用函数模板对函数指针进行初始化或赋值,这样做的时候,编译器使用指针的类型实例化具有适当模板实参的模板版本。

    template <typename T> int compare(const T&, const T&);
    
    // pf1 points to the instantiation int compare (const int&, constint&)
    
    int (*pf1) (const int&, const int&) = compare;

    获取函数模板实例化的地址的时候,上下文必须是这样的:它允许为每个模板形参确定唯一的类型或值。如果不能从函数指针类型确定模板实参,就会出错。

    // overloaded versions of func; each take a different function pointer type
    
    void func(int(*) (const string&, const string&));
    
    void func(int(*) (const int&, const int&));
    
    func(compare); // error: which instantiation of compare?

    在返回类型中使用类型形参

    指定返回类型的一种方式是引入第三个模板形参,它必须由调用者显式指定:

    // T1 cannot be deduced: it doesn't appear in the function parameterlist
    
    template <class T1, class T2, class T3>
    
    T1 sum(T2, T3);
    
    // ok T1 explicitly specified; T2 and T3 inferred from argument types
    
    long val3 = sum<long>(i, lng); // ok: calls long sum(int, long)

    显式模板实参从左至右对应模板形参相匹配,第一个模板实参与第一个模板形参匹配,第二个实参与第二个形参匹配,以此类推。

    // poor design: Users must explicitly specify all three template parameters
    
    template <class T1, class T2, class T3>
    
    T3 alternative_sum(T2, T1);
    
    // error: can't infer initial template parameters
    
    long val3 = alternative_sum<long>(i, lng);
    
    // ok: All three parameters explicitly specified
    
    long val2 = alternative_sum<long, int, long>(i, lng);

    模板编译模型

    编译器实例化特定类型的模板的时候,编译器必须能够访问定义模板的源代码。当调用函数模板或类模板的成员函数的时候,编译器需要函数定义,需要那些通常放在源文件中的代码。标准 C++ 为编译模板代码定义了两种模型。

    在包含编译模型中,编译器必须看到用到的所有模板的定义。一般而言,可以通过在声明函数模板或类模板的头文件中添加一条 #include 指示使定义可用,该#include 引入了包含相关定义的源文件:

    // header file utlities.h
    
    #ifndef UTLITIES_H // header gaurd (Section 2.9.2, p. 69)
    
    #define UTLITIES_H
    
    template <class T> int compare(const T&, const T&);
    
    // other declarations
    
    #include "utilities.cc" // get the definitions for compare etc.
    
    #endif
    
    // implemenatation file utlities.cc
    
    template <class T> int compare(const T &v1, const T &v2)
    
    {
    
    if (v1 < v2) return -1;
    
    if (v2 < v1) return 1;
    
    return 0;
    
    }
    
    // other definitions

    在分别编译模型中,编译器会为我们跟踪相关的模板定义。但是,我们必须让编译器知道要记住给定的模板定义,可以使用 export 关键字来做这件事。对类模板使用 export 更复杂一些。通常,类声明必须放在头文件中

    // class template header goes in shared header file
    
    template <class Type> class Queue { ... };
    
    // Queue.ccimplementation file declares Queue as exported
    
    export template <class Type> class Queue;
    
    #include "Queue.h"
    
    // Queue member definitions

    导出类的成员将自动声明为导出的。也可以将类模板的个别成员声明为导出的,在这种情况下,关键字 export 不在类模板本身指定,而是只在被导出的特定成员定义上指定。

     

    类模板的 static 成员

    template <class T> class Foo {
    
    public:
    
    static std::size_t count() { return ctr; }
    
    // other interface members
    
    private:
    
    static std::size_t ctr;
    
    // other implementation members
    
    };

    每个实例化表示截然不同的类型,所以给定实例外星人所有对象都共享一个static 成员。因此,Foo<int> 类型的任意对象共享同一 static 成员 ctr,Foo<string> 类型的对象共享另一个不同的 ctr 成员。

     

    通常,可以通过类类型的对象访问类模板的 static 成员,或者通过使用作用域操作符直接访问成员。当然,当试图通过类使用 static 成员的时候,必须引用实际的实例化:

    Foo<int> fi, fi2; // instantiates Foo<int> class
    
    size_t ct = Foo<int>::count(); // instantiates Foo<int>::count
    
    ct = fi.count(); // ok: uses Foo<int>::count
    
    ct = fi2.count(); // ok: uses Foo<int>::count
    
    ct = Foo::count(); // error: which template instantiation?

    与任意其他成员函数一样,static 成员函数只有在程序中使用时才进行实例化。

     

    像使用任意其他 static 数据成员一样,必须在类外部出现数据成员的定义。在类模板含有 static 成员的情况下,成员定义必须指出它是类模板的成员:

    template <class T> size_t Foo<T>::ctr = 0; // define and initialize ctr

     

    一个泛型句柄类

    /* generic handle class: Provides pointerlike behavior. Although access through
    
    * an unbound Handle is checked and throws a runtime_error exception.
    
    * The object to which the Handle points is deleted when the last Handle goes away.
    
    * Users should allocate new objects of type T and bind them to a Handle.
    
    * Once an object is bound to a Handle,, the user must not delete that object.
    
    */
    
    template <class T> class Handle {
    
    public:
    
    // unbound handle
    
    Handle(T *p = 0): ptr(p), use(new size_t(1)) { }
    
    // overloaded operators to support pointer behavior
    
    T& operator*();
    
    T* operator->();
    
    const T& operator*() const;
    
    const T* operator->() const;
    
    // copy control: normal pointer behavior, but last Handle deletes the object
    
    Handle(const Handle& h): ptr(h.ptr), use(h.use)
    
    { ++*use; }
    
    Handle& operator=(const Handle&);
    
    ~Handle() { rem_ref(); }
    
    private:
    
    T* ptr; // shared object
    
    size_t *use; // count of how many Handle spointto *ptr
    
    void rem_ref()
    
    { if (--*use == 0) { delete ptr; delete use; } }
    
    };
    
     
    
    template <class T>
    
    inline Handle<T>& Handle<T>::operator=(const Handle &rhs)
    
    {
    
    ++*rhs.use; // protect against self-assignment
    
    rem_ref(); // decrement use count and delete pointers if
    
    needed
    
    ptr = rhs.ptr;
    
    use = rhs.use;
    
    return *this;
    
    }
    
     
    
    template <class T> inline T& Handle<T>::operator*()
    
    {
    
    if (ptr) return *ptr;
    
    throw std::runtime_error("dereference of unbound Handle");
    
    }
    
    template <class T> inline T* Handle<T>::operator->()
    
    {
    
    if (ptr) return ptr;
    
    throw std::runtime_error("access through unbound Handle");
    
    }
    
     
    
    template <class T> inline const T* Handle<T>::operator->() const
    
    {
    
               if (ptr) return ptr;
    
    else throw std::logic_error("unbound Sales_item");
    
     }
    
     
    
    template <class T> inline const T& Handle<T>:: const
    
    {
    
               if (ptr) return *ptr;
    
    else throw std::logic_error("unbound Sales_item");
    
    }
    
     
    
    class Sales_item {
    
    public:
    
    // default constructor: unbound handle
    
    Sales_item(): h() { }
    
    // copy item and attach handle to the copy
    
    Sales_item(const Item_base &item): h(item.clone()) { }
    
    // no copy control members: synthesized versions work
    
    // member access operators: forward their work to the Handle class
    
    const Item_base& operator*() const { return *h; }
    
    const Item_base* operator->() const
    
    { return h.operator->(); }
    
    private:
    
    Handle<Item_base> h; // use-counted handle
    
    };
    
     

    模板特化

    模板特化(template specialization)是这样的一个定义,该定义中一个或多个模板形参的实际类型或实际值是指定的。特化的形式如下:

    • 关键字 template 后面接一对空的尖括号(<>);

    • 再接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参;

    • 函数形参表;

    • 函数体。

    template <typename T>
    
    int compare(const T &v1, const T &v2)
    
    {
    
    if (v1 < v2) return -1;
    
    if (v2 < v1) return 1;
    
    return 0;
    
    }
    
     
    
    // special version of compare to handle C-style character strings
    
    template <>
    
    int compare<const char*>(const char* const &v1,
    
    const char* const &v2)
    
    {
    
    return strcmp(v1, v2);
    
    }

    模板特化必须总是包含空模板形参说明符,即 template<>,而且,还必须包含函数形参表。如果可以从函数形参表推断模板实参,则不必显式指定模板实参:

    // error: invalid specialization declarations
    
    // missing template<>
    
    int compare<const char*>(const char* const&,
    
    const char* const&);
    
    // error: function parameter list missing
    
    template<> int compare<const char*>;
    
    // ok: explicit template argument const char* deduced from parameter types
    
    template<> int compare(const char* const&, const char* const&);

    当定义非模板函数的时候,对实参应用常规转换;当特化模板的时候,对实参类型不应用转换。在模板特化版本的调用中,实参类型必须与特化版本函数的形参类型完全匹配,如果不完全匹配,编译器将为实参从模板定义实例化一个实例。

     

    与其他函数声明一样,应在一个头文件中包含模板特化的声明,然后使用该特化的每个源文件包含该头文件。

     

    普通作用域规则适用于特化

    在能够声明或定义特化之前,它所特化的模板的声明必须在作用域中。类似地,在调用模板的这个版本之前,特化的声明必须在作用域中:

    // define the general compare template
    
    template <class T>
    
    int compare(const T& t1, const T& t2) { /* ... */ }
    
    int main() {
    
    // uses the generic template definition
    
    int i = compare("hello", "world");
    
    // ...
    
    }
    
    // invalid program: explicit specialization after call
    
    template<>
    
    int compare<const char*>(const char* const& s1,
    
    const char* const& s2)
    
    { /* ... */ }

    这个程序有错误,因为在声明特化之前,进行了可以与特化相匹配的一个调用。当编译器看到一个函数调用时,它必须知道这个版本需要特化,否则,编译器将可能从模板定义实例化该函数。

     

    重载与函数模板

    函数模板可以重载:可以定义有相同名字但形参数目或类型不同的多个函数模板,也可以定义与函数模板有相同名字的普通非模板函数。

     

    如果重载函数中既有普通函数又有函数模板,确定函数调用的步骤如下:

    1. 为这个函数名建立候选函数集合,包括:

    a. 与被调用函数名字相同的任意普通函数。

    b. 任意函数模板实例化,在其中,模板实参推断发现了与调用中所用函数实参相匹配的模板实参。

    2. 确定哪些普通函数是可行的(第 7.8.2 节)(如果有可行函数的话)。候选集合中的每个模板实例都 可行的,因为模板实参推断保证函数可以被调用。

    3. 如果需要转换来进行调用,根据转换的种类排列可靠函数,记住,调用模板函数实例所允许的转换是有限的。

    a. 如果只有一个函数可选,就调用这个函数。

    b. 如果调用有二义性,从可行函数集合中去掉所有函数模板实例。

    4. 重新排列去掉函数模板实例的可行函数。

    • 如果只有一个函数可选,就调用这个函数。

    • 否则,调用有二义性。

     

    设计既包含函数模板又包含非模板函数的重载函数集合是困难的,因为可能会使函数的用户感到奇怪,定义

    函数模板特化几乎总是比使用非模板版本更好。

  • 相关阅读:
    滚动页面时DIV到达顶部时固定在顶部
    【Java学习笔记】拾遗
    【Java学习笔记】文件信息
    【Java学习笔记】使用BufferedReader类(流的读写)
    【Java学习笔记】可变参数
    【Java学习笔记】控制台读写
    【Java学习笔记】关于默认值
    【Java学习笔记】FileChannel的学习
    【JAVA学习笔记】静态导入
    【Java学习笔记】Java中关于tostring方法的误操作
  • 原文地址:https://www.cnblogs.com/1zhk/p/5131977.html
Copyright © 2011-2022 走看看