[C++]模板与泛型编程

模板与泛型编程

本文尝试着介绍对泛型编程的理解,从而扩展我们的template编程。泛型编程是C++中非常重要的一部分,它使得我们节省了很多编写不同代码的体力。

1. 了解隐式接口和编译器多态

与OOP的不同之处

面向对象编程世界总是以显式接口运行期多态解决问题。

例如:

void doProcessing( Widget &w) {
    if (w.size() > 10 && w != someWidget) {
        Widget temp(w);
        temp.normalize();
        temp.swap(w);
    }
}
  • 就像以上的函数,我们总是可以通过查找Widget,发现Widget里面的显式接口,而这些在源码中明确可见(这就是显式接口)。
  • 由于Widget的某些成员函数是virtual,w对这些函数的调用将表现出运行期多态,也就是在运行期根据具体的w动态类型决定调用哪个函数。

但在泛型编程的世界里就截然不同了。在此世界里,显式接口和运行时多态仍然存在,只不过不再那么重要了。反而是隐式接口编译器多态就变得更重要了。我们把上面的函数改成template。

void doProcessing(T &w) {
    if (w.size() > 10 && w != someWidget) {
        T temp(w);
        temp.normalize();
        temp.swap(w);
    }
}

现在,

  • w必须支持哪一种种接口由template中执行在w身上的凑战偶来解决。而这些操作就是T必须支持的一组隐式接口。
  • 不同的template参数会在编译器具现化(instantiated)不同的函数从而使得函数可以被调用,这就是所谓的编译器多态。

显式接口和隐式接口

我们进一步的区别这两种接口。

通常,显式接口是由函数的签名式(也就是函数名称,参数类型,返回类型)构成。就好像在一个class的public接口中会有构造函数、析构函数、各类成员函数、还有typedef等等。

隐式接口就不同了。它并不基于函数签名式,而是由有效表达式组成。

void doProcessing(T &w) {
    if (w.size() > 10 && w != someWidget) {
        T temp(w);
        temp.normalize();
        temp.swap(w);
    }
}

就像前面这个例子,T的隐式接口总有某些约束。

  • 它必须提供一个size类型,换回整型。
  • 总是swap操作,!=操作符,temp.normalize函数等等。

加诸与template参数上的隐式接口,就像class上的显示接口一样都会在编译器进过检查,从而确保程序的正常运行。

2. 了解typename的双重含义

一般都认为typename和class的意义是相同的,但只在确定template类型时是如此。在以下接种情况下,typename有着特别的意义。

在template指涉(refer to)两种名称

template <typename T>
void print(T& container) {
    T::types temp = 10;
    cout << temp.get() << endl;
}

以上template实际上是无法通过编译的。我们本来希望temp的类型是T中typedef的类型types,但实际上编译器不这么认为。

在template内出现的名称如果相依于某个template参数,称之为从属名称,如果从属名称在class内呈嵌套状,就叫做嵌套从属名称。T::types实际上就是个嵌套从属名称,并且指涉某个类型。

嵌套从属名称可能会导致解析困难,就比如说

T::types* temp = 10;

编译器会认为types是一个变量名,所以就是types 乘上 temp!很奇怪吧!C++有个规则可以解析此歧义状态:如果解析器在template中遭遇一个嵌套从属名称,它便假设这个名称不是个类型。而准确的做法是用

typename <#qualifier#>::<#name#>

于是函数就应该是:

template <typename T>
void print(T& container) {
    typename T::types temp = 10;
    cout << temp.get() << endl;
}

并且根据template的隐式接口设计相应的类:

template <typename T>
class Type {
public:
    Type(int y) : x(y) {
    }
    Type& operator =(int &y) {
        x = y;
        return *this;
    }
    int get() {
        return x;
    }
private:
    int x;
};
template <typename T>
class Derived {
public:
    Derived(int y) : x(y) {
    }
    void print() {
        cout << Base<T>::get() << endl;
    }
    typedef Type<T> types;
private:
    int x;
};
template <typename T>
void print(T& container) {
    typename T::types temp = 10;
    cout << temp.get() << endl;
}
int main () {
    Derived<int> temp(10);
    print(temp);
    return 0;
}
/* output: 10 */

所以一般地规则就是:任何时候当你想要在template中指涉一个嵌套从属类型名称,就应该在其前面加上关键字typename。

不可以使用typename的情况

还有一个意外情况就是:typename不可以出现在base class list内的嵌套从属类型名称之前,也不在member initialization list中作为base class修饰符。

所以使用继承关系的template就应该是如下的语法:

template <typename T>
class Base {
public:
    Base(int y) : x(y) {
    }
    int get() {
        return x;
    }
private:
    int x;
};
template <typename T>
class Type {
public:
    Type(int y) : x(y) {
    }
    Type& operator =(int &y) {
        x = y;
        return *this;
    }
    int get() {
        return x;
    }
private:
    int x;
};
template <typename T>
class Derived : public Base<T> {
public:
    Derived(int x) : Base<T>::Base(x) {
    }
    void print() {
        cout << Base<T>::get() << endl;
    }
    typedef Type<T> types;
};

另外,因为每一次使用嵌套从属类型名称都是打一大串的代码,所以为了防止出错,最好使用typedef把相应的代码简化。

3. 学习处理模板化基类内的名称

假设我们需要对不同company设计不同类来保存各自的信息,并且用一个类来发送信息。

class companyA {
public:
    companyA(string msgs) : msg(msgs) {
    }
    void print_message() {
        cout << msg << endl;
    }
private:
    string msg;
};

class companyB {
public:
    companyB(string msgs) : msg(msgs) {
    }
    void print_message() {
        cout << msg << endl;
    }
private:
    string msg;
};

template <typename Company>
class MsgSender {
public:
    void Send(Company& orig) {
        orig.print_message();
    }
};
template <typename Company>
class Loger : public MsgSender<Company> {
public:
    void Send_msg(Company& orig) {
        Send(orig); // error!
    }
};

int main () {
    companyA temp("yan");
    companyB tempB("ze");
    Loger<companyA> tempA;
    tempA.Send_msg(temp);
    return 0;
}

这个类是有问题的。原因就在于编译器找不到关于Send函数的定义。就算很明显的在基类中有Send函数。原因就在于当编译器遇到Loger的定义式时,并不知道将继承什么样的class,就算知道是继承MsgSender,它也不知道这个类中是否有Send这个函数。

具体的说,就是有个特别的类,如:

class companyC {
public:
    companyC(string msgs) : msg(msgs) {
    }
    void print() {
        cout << msg << endl;
    }
private:
    string msg;
};

使得MsgSender没有办法正常地调用print_message()函数,于是出错。解决方法就是特化模板:

template <>
class MsgSender<companyC>  { public: void Send(companyC& orig) { orig.print(); }
};

问题还没有解决完。编译器仍然找不到关于Send的定义。因为编译器往往拒绝在template base class内寻找继承而来的名称,所以解决方法就是强制他调用。

方法一:加上this->

template <typename Company>
class Loger : public MsgSender<Company> {
public:
    void Send_msg(Company& orig) {
        this->Send(orig);
    }
};

方法二:使用using声明式

template <typename Company>
class Loger : public MsgSender<Company> {
public:
    using MsgSender<Company>::Send;
    void Send_msg(Company& orig) {
        Send(orig);
    }
};

方法三:明确指出调用函数的作用域

template <typename Company>
class Loger : public MsgSender<Company> {
public:
    void Send_msg(Company& orig) {
        MsgSender<Company>::Send(orig);
    }
};

这个方法往往最不好,因为他限制了virtual绑定行为。

完整的实例程序:

#include <iostream> // std::cout
using namespace std;

class companyA {
public:
    companyA(string msgs) : msg(msgs) {
    }
    void print_message() {
        cout << msg << endl;
    }
private:
    string msg;
};

class companyB {
public:
    companyB(string msgs) : msg(msgs) {
    }
    void print_message() {
        cout << msg << endl;
    }
private:
    string msg;
};

class companyC {
public:
    companyC(string msgs) : msg(msgs) {
    }
    void print() {
        cout << msg << endl;
    }
private:
    string msg;
};

template <typename Company>
class MsgSender {
public:
    void Send(Company& orig) {
        orig.print_message();
    }
};
template <typename Company>
class Loger : public MsgSender<Company> {
public:
    void Send_msg(Company& orig) {
        MsgSender<Company>::Send(orig);
    }
};

template <>
class MsgSender<companyC>  {
public:
    void Send(companyC& orig) {
        orig.print();
    }
};

int main () {
    companyA tempA("yan");
    companyB tempB("ze");
    companyC tempC("xin");
    Loger<companyA> temp;
    temp.Send_msg(tempA);
    return 0;
}
/* output: yan */

4. 运用成员函数模板接受所有兼容类型

真实指针做得很好的意见事情是,支持隐式类型转换,从而实现了动态绑定。但是在template中却并不会自动具有这个功能,需要我们自己写出成员函数模板。

问题产生

本节主要探讨智能指针初始化的问题。
假设我们需要做一个继承体系:

class Top { ... };
class Middle : public Top { ... };
class Bottom : public Middle { ... };
Top* pt1 = new Middle;
Top* pt2 = new Bottom;
const Top* pct2 = pt1;

如果我们想要写出一个SmartPtr的类来完成对智能指针的封装,我们需要注意一些问题。在同一个template的不同具现化之间并不存在什么与生俱来的固有关系(就比如说,带有base-dereived关系的b、d两类型分别具现化某个template,产生出来的是两个不带有base-dereived关系的具现化类),所以编译器并不认为SmartPtr和SmartPtr之间有什么关系。所以为了获得转换关系,我们需要明确地写出来。

Template和泛型编程

简单地说,根据泛型编程的思维,我们总是希望能够写出一个泛型的构造函数并附加一些判断(其是否具有base-dereived关系)来满足各式各样的情况。方法就是使用成员函数模板

template SmartPtr {
public:
    template<typename U>
    SmartPtr(const SmartPtr<U>& other) : 
    heldPtr(other.get()) { ... }
    T* get() const { return heldPtr; }
    ...
private:
    T* heldPtr;
}

这个代码中必须注意,不能写成explicit的,因为我们总是希望能够进行隐式转换。其次我们用other的成员get返回一个该类型的指针,在此初始化中,编译器会判断这两个指针之间是否可以隐式转换,从而确认这两者之间是否真的有相应关系。

shared_ptr摘录

[C++]模板与泛型编程_第1张图片

[C++]模板与泛型编程_第2张图片

泛化copy构造函数具现化原理

member function template是个奇妙东西,它并不改变语言基本规则,而这意味着,你声明了一个泛化copy构造函数,并不阻止编译器生成他们自己的copy构造函数。也就是说,如果你写了一个泛化的copy构造函数,而此时恰好U和Y的类型恰好相同,编译器会调用自己生成的copy构造函数。所以为了能够准确的控制类。最好还是两种copy 构造函数都写上吧。

[C++]模板与泛型编程_第3张图片

5. 需要类型转换时请为模板定义非成员函数

本节我们讨论如何把下面这个函数转换为template。

class Divd {
public:
    Divd(int n1s = 0, int n2s = 1) : n1(n1s), n2(n2s) {
    }
    int n1;
    int n2;
};
Divd operator*(const Divd& orig1, const Divd& orig2) {
    Divd temp(orig1.n1 * orig2.n1, orig1.n2 * orig2.n2);
    return temp;
}
int main () {
    Divd temp = Divd(4, 2) * 2;
    cout << temp.n1 << "," << temp.n2 << endl;
    return 0;
}
/*output: 8, 2 */

问题产生

一开始我们认为这个问题非常简单:

template <typename T>
class Divd {
public:
    Divd(int n1s = 0, int n2s = 1) : n1(n1s), n2(n2s) {
    }
    int n1;
    int n2;
};
template <typename T>
Divd<T> operator*(const Divd<T>& orig1, const Divd<T>& orig2) {
    Divd<T> temp(orig1.n1 * orig2.n1, orig1.n2 * orig2.n2);
    return temp;
}
// in main:
...
    Divd<int> temp = Divd<int>(4, 2) * 2;

但随后我们发现这个程序编译无法通过。问题就在于模板化的divd和non-template的divd是不同的。理解这个问题我们需要仔细分析operator*调用中的实参类型推断。

Divd<int>(4, 2)

这一部分的推导并不困难,说明T就是int了。

但是第二个参数开始出现问题了。本来第二个参数应该是const Divd& 的类型,但调用过程却给出了int,编译器该如何推算出T?我们当然希望避免能够隐式转换把int转换为const Divd&,但因为在template实参推导过程中从不将隐式类型转换函数纳入考虑。这样的转换在函数调用过程中的确被使用,但在调用一个函数之前,首先必须知道那个函数存在,而为了知道他,必须先为相关function template推导出相关的参数类型,然而在template实参推导过程中并不考虑采纳“通过构造函数而发生的”隐式类型转换,于是以失败告终。

解决方法:使用friend

在template class内的friend声明式可以指涉某个特定函数。class template并不依赖template实参推导(后者指用子啊function template),所以编译器总是能够在class divd具现化是得知T。还有一个问题就是,如果我们声明了一个函数,就应该给出相应的实现,所以operator*的实现代码必须写进class中,否则会出现连接错误。

正确代码:

template <typename T>
class Divd {
public:
    Divd(int n1s = 0, int n2s = 1) : n1(n1s), n2(n2s) {
    }
    int n1;
    int n2;
    friend Divd operator*(const Divd& orig1, const Divd& orig2) {
        Divd temp(orig1.n1 * orig2.n1, orig1.n2 * orig2.n2);
        return temp;
    }
};

最后补充一点,把代码写进class声明式中,意味着隐式地把它声明为inline,如果代码量太大时,最后是在class外再写一个具体的实现过程,然后在class内的函数中调用那个函数,从而减少目标码的量。

6. 使用traits class表示类型信息

trait class表示一个类所具有的类型信息,这种class在STL中广泛使用。

问题产出

STL主要由“泳衣表现容器、迭代器和算法”的template构成,但也覆盖若干工具性的template,其中一个为advance,用来将某个迭代器移动某个距离。

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d);

我们希望这个advance能够一步到位,达到我们希望到达的iter。但事实上,并不是每一种迭代器都能使用+=操作。首先我们先来介绍了一下迭代器的分类。

迭代器分类

STL公有5中迭代器分类,对应他们支持的操作。

  • input迭代器:只能向前移动,一次一步,客户只能读取(不可写)他们所指的东西,而且只能读一次。(istream_iterator)
  • output迭代器:一切只为输出,只能向前移动,一次一步,客户只可涂写所指的东西,而且只能涂写一次。(ostream_iterator)

(以上两种迭代器只适合“一次性操作算法)

  • forward迭代器:这种迭代器可以做上述两种迭代器的事情,而且可以读或写其所指物一次以上,这可施行于多次性操作算法。
  • bidirectional迭代器:这种迭代器可以双向移动(例如list、set、map等的迭代器)
  • random access迭代器,可以执行迭代器算法。(可以在常量时间向前或向后移动任意距离)

对于以上5中迭代器,C++标准程序库分别提供专属的卷标结构(tag struct)

    struct input_iterator_tag {};
    struct output_iterator_tag {};
    struct forward_iterator_tag : public input_iterator_tag {};
    struct bidirectional_iterator_tag : public forward_iterator_tag {};
    struct random_access_iterator_tag : public bidirectional_iterator_tag {};

问题解决

我们回到原来的advance里。

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
    if ( /* iter is a random access iterator) {
        iter += d;
    } else {
        if (d >= 0) {
            while (d--) {
                iter++;
            }
        } else {
            while (d++) {
                iter--;
            }
        }
    }
}

这我们希望实现的样子。所以问题的关键在于我们要知道IterT是否为random access迭代器分类,也就是知道这个类型的某些信息。那就trait做的事:他们允许我们在编译器间区别某些类型信息。

Trait并不是c++关键字或一个预先定义好的构件:他们是一种计数,也是一个c++程序员共同遵守的协议。它的技术要求之一是,他对内置类型和用户自定义类型的表现必须一样好。(也就是说,就算传进去的是const char* 和 int,也一样能够正常的运行)

因为”trait必须能够施行于内置类型“意味着”类型内的嵌套信息“这种东西是无法实现的,因为我们无法将信息嵌套于原始指针内,因此类型的trait信息必须位于类型之外。标准技术是把它放进一个template及其一个或多个特化版本。

在deque里面嵌套一个iterator类,并且在这个iterator里面声明一个iterator_category用来确认IterT的迭代器分类。

template<...>
class deque {
public:
    class iterator {
    public:
        typedef random_access_iterator_tag iterator_category;
        ...
    };
    ...
};

至于iterator_traits。

template<typename IterT>
struct iterator_traits {
    typedef typename IterT::iterator_category iterator_category;
    ...
};

这对用户自定义类型是行得通的,但对指针是行不通的,因为指针不可能嵌套typedef。所以我们需要针对指针提供偏特化版本。

template<typename IterT>
struct iterator_traits<IterT*> {
    typedef random_access_iterator_tag iterator_category;
    ...
};

现在我们知道应该如何设计并实现一个trait class了:

  • 确认若干你希望将来可取得的类型相关信息。例如迭代器而言,我们希望将来可取的其分类。
  • 为该信息选择一个名称。
  • 提供一个template和一组特化版本,内含你希望支持的类型相关信息。

所以之前的advance可以这么实现:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
    if (typeid(typename std::iterator_traits<IterT>::iterator_category) == typeid(std::random_access_iterator_tag)) {
        ...
    }
    ...
}

这虽然看起来可以完成任务,但会导致编译问题。因为在编译器中IterT类型可知,std::iterator_traits::iterator_category的类型也可知,但if语句却是在运行期才会核定。这不仅浪费时间,也造成可执行文件膨胀。

所以我们需要一个条件式,判断“编译器核定成功”之类型。而重载就可以完成任务。

photo:

有了这些doAdvance重载版本,advance需要做的只是调用它们并额外传递一个对象,后者必须带有适当的迭代器分类。于是编译器运用重载解析机制调用适当的实现代码:

photo:

我们可以总结如何使用traits class了。

  • 建立一组重载函数或函数模板,彼此间的差异只在于各自的trait参数。令每个函数实现码与其接受之traits信息相应和。
  • 建立一个控制函数或函数模板,它调用上述那些重载函数并传递trait class所提供的信息。

traits广泛用于标准程序库,所以了解这个机制对我们理解库有重要的意义。

你可能感兴趣的:([C++]模板与泛型编程)