C++基础——关于模板的技巧性基础知识(typename、成员模板、模板的模板参数)

来源:互联网 发布:mac如何设置开机密码 编辑:程序博客网 时间:2024/06/07 01:36

  • typename
    • template
  • 成员模板
  • 模板的模板
    • 模板的模板 的实参匹配

本文继续深入探讨模板的基础知识,covers 内容如下:

  • 关键字typename的另一种用法
  • 将成员函数和嵌套类也定义成模板
  • 模板的模板参数(template template parameters)

typename

C++标准化的过程中,引入关键字typename是为了说明:模板类型参数内部的标识符(associated type,常见于STL中的各种容器)也可以是一个类型:

template<typename T>class MyClass{    typename T::SubType* ptr;}

上述程序中,第二个typename被用来说明,SubType是定义与类T内部的一种类型,也就是associated type,因而,ptr是一个指向T::SubType类型的指针。如果不使用typenameT::SubType会被优先看做T的一个静态成员,也就是一个具体而变量或对象,于是,下面的表达式:

T::SubType * ptr;

会被看做类T的静态成员SubType和ptr的乘积。

通常情况下,当要以类型的形式使用类型模板参数的associated types(即定义于其内部的类型)时,就必须使用typename关键字。

下面我们来考虑一个typename的典型应用场景,即在模板代码中访问STL容器的迭代器(const_iterator,作为每一个容器类的associated type)。

template<typename COLL>void printColl(ostream& os, const COLL& coll){    typename COLL::const_iterator pos;    typename COLL::const_iterator end(coll.end());    for(pos = coll.begin(); pos != end; ++pos)        os << *pos << " ";    os << endl;}

.template

我们以一个小例子说明这一语法特性:

虽然bitset<>模板类提供了简单的与string交互(转换)的接口,如bitset<>的string参数类型的构造函数,也有十分方便的to_string的成员函数,但两者都是成员模板,就不能像一般的成员函数那样使用。关于这部分内容更详尽的内容请见

template<size_t N>string to_string(const bitset<N>& b){    return b.template            to_string<char, char_traits<char>,                 allocator<char> >();}

这个函数的目的是对b.to_string这一成员函数模板赋值的封装,

string s = b.template to_string<char, char_traits<char>,                            allocator<char> >();// 借助于上面的辅助函数string s = to_string<6>(b);

在老式的c++编译器中,这里的.template不能省略,如果没有这个.template,编译器无法确定,其后的<是小于号,还是模板参数列表的起始符号。

成员模板

类的成员也可以是模板。嵌套类和成员函数都可以作为模板。

我们在之前的讨论

template<typename T>class Stack{public:    void push(const T& x)     { elems.push_back(x);}              // 尾端入    void pop() { elems.pop_back();}     // 尾端出,实现一种FILO的机制    T& top() { return elems.back();}    const T& top() const { return elems.back();}    bool empty() const { return elems.empty();} private:    std::deque<T> elems;}

通常而言,栈之间只有在元素类型完全相同时才能相互赋值,换句话说,类型不同的栈,无法进行赋值,即使这两种(元素的)类型之间存在隐式类型抓换,如:

Stack<int> is1, is2;Stack<double> ds3;//...is1 = is2;  // OK: 具有相同类型的栈 is3 = is1;  // ERROR:两边栈的类型不同

类中国缺省赋值运算符要求=两边具有相同的元素类型。通过定义一个模板形式的赋值运算符,元素类型不同的两个栈就可实现相互赋值。

template<typename T>class Stack{public:    // ...    template<typename T2>    Stack<T>& operator=(const Stack<T2>& rhs);            // 返回值不含const 属性,保证了`s1 = s2 = s3`}

新的赋值运算符的实现大致如下:

template<typename T>    template<typename T2>Stack<T>& Stack<T>::operator=(const Stack<T2>& rhs){    if ((void*)this == (void*)&rhs)        return *this;       // identity test,同一性测试    Stack<T2> tmp(rhs);    elems.clear();    while(!tmp.empty())    {        elems.push_back(tmp.top());        tmp.pop();    }    return *this;}

新的类型检查发生在:

elems.push_back(tmp.top());

同样地,在实现中,也可以把内部容器类型实现为一个模板参数:

// CONT类型的容器,必须支持Stack<, >类模板体内的操作或者接口// 如push_back, pop_back, back, emptytemplate<typename T, typename CONT = std::deque<T> >class Stack{public:    void push(const T& x) { elems.push_back(x);}    void pop() { elems.pop_back();}    T& top() { return elems.back();}    const T& top() const { return elems.back();}    bool empty() const { return elems.empty();}    template<typename T2, typename CONT2>    Stack<T, CONT>& operator=(const Stack<T2, CONT2>& rhs);private:    CONT elems;}
template<typename T, typename CONT>    template<typename T2, typename CONT2>Stack<T, CONT>& Stack<T, CONT>::operator=(const Stack<T2,  CONT2>& rhs){    if((void*)this == (vois*)&rhs)        return *this;    Stack<T2, CONT2> tmp(rhs);    elems.clear();    while (!tmp.empty())    {        elems.push_back(tmp.top());        tmp.pop();    }    return *this;}

模板的模板

这是一种口语化的说法,准确地说,以模板作为另一个模板的模板参数,是不是更绕,:-D。我们继续以Stack模板类举例,探讨模板的模板存在的必要性,再往大点说是C++模板技术演化的线索。

// 这时的第二个模板参数是一种模板的实例化// 必须这样传递,Stack<int, deque<int> > // Stack<int, queue<int> >template<typename T, typename CONT = std::deque<T> >class Stack{...};Stack<int, std::vector<int> > intStack;

我们看到上述代码是存在冗余的(忽然想到一句话,模板类是不是可以称得上一种具有参数的类呢,正如函数一样。),即第一个模板参数是第二个模板参数的参数,且有可能发生参数的传递错误,如Stack<int, deque<double> >。我们猜想是否存在如下的没有冗余的声明式:

Stack<int, std::vector> intStack;

为了支持这样的语法特性:

template<typename T, template<typename ELEM>                    class CONT = std::deque >class Stack{public:    void push(const T& x);    void pop();    T& top();    const T& top() const;    bool empty() const { return elems.empty();}private:    CONT<T> elems;}

这与前述代码的不同之处在于,第二个参数从一个类模板实例化变成了一个类模板,,缺省值自然也从std::deque<T>变成了std::deque,在使用时,第二个参数必须是一个类模板,并且由第一个模板参数传递进来的类型进行实例化(这个例子比较特殊,一般地可以在类模板内部以任意允许的类型实例化模板的模板参数):

CONT<T> elems;

之前的讲述模板基础知识的时候,我们曾说过,作为模板参数的声明,通常可以使用typename替换关键字class,然而,上述的CONT是欲定义的一个模板类,必须用关键字class修饰。

template<typename T, template<typename ELEM>                    class CONT = std::deque>class Stack { ... };        // 正确template<typename T, template<typename ELEM>                    typename CONT = std::deque>class Stack { ... }         // 错误

就像我们可以在.hpp文件的类成员函数或者全局函数的声明中省略形参名,保留形参类型一样,我们也可以对这里的作为模板参数的模板的参数,也即ELEM,因为不涉及的函数体,或者即是涉及函数体,在函数体内也用不到,可见在真实的内存模型中,操作的内存单元,而不是形参名(大概保存在一个叫符号表的结构中,这部分内容可参阅编译原理或者C++内存模型相关细节)。

template<typename T, template<typename>                        class CONT = std::deque>class Stack{ ... } 

另外,还必须对成员函数的成员函数的声明进行相应的修改,将第二个模板参数指定为模板的模板参数:

template<typename T, template<typename> class CONT>void Stack<T, CONT>::push(const T& x){    elems.push_back(x);}

模板的模板 的实参匹配

这时如果不加修改的使用新版本的Stack,甚至不调用客户端代码,不进行实例化,在编译阶段就将得到一个错误信息,缺省值std::deque和模板的模板参数CONT并不匹配。问题在于,模板的模板实参(比如本例中的std::deque)也是一个具有参数(以A为例)的模板(std::deque的模板参数如下所示,也是一个具有两个模板参数的模板):

template<class _Ty,    class _Alloc = allocator<_Ty> >    class deque             : public 

它将替换这里模板的模板参数(也即是这里的CONT),而模板的模板参数是一个具有参数B的模板(也即是本例的template)匹配过程要求参数A和参数B必须完全匹配,而这里,如前所示,并不能完全匹配,一个是两个模板参数的类模板,而一个只有一个模板参数 。这时我们可以重写类的声明,以使参数个数相匹配。

template<typename T, template<typename ELEM, template ALLOC = std::allocator<ELEM>                     class CONT = std::deque>class Stack{private:    CONT<T> elems;          // CONT<T, allocator<T> >                             // deque<T, allocator<T> >}

同样这时,ALLOC也可省略不写,因为在实现中并不会用到。现在我们便可以写出完整版的Stack的声明与定义了:

template<typename T, template<typename ELEM, typename =                        std::alloctor<ELEM> >                     class CONT>class Stack{public:    void push(const T&);    void pop();    T& top();    const T&() const;     bool empty() const;    // 成员模板    template<typename T2, template<typename ELEM2,                         typname ALLOC=std::allocator<ELEM2> >                        class CONT2>    Stack<T, CONT>& operator=(const Stack<T2, CONT2>&);private:    CONT<T> elems;              // 这里完成实例化}template<typename T, template<typename, typename>                    class CONT>void Stack<T, CONT>::push(const T& x){    elems.push_back(x);}///template<typename T, template<typename, typename>                    class CONT>    template<typename T2, template<typename, typename>                          class CONT2>Stack<T, CONT>& Stack<T, CONT>::operator=(const Stack<T2, CONT2>& rhs){    if ((void*)this == (void*)&rhs)         // identity test        return *this;    Stack<T2, CONT2> tmp(rhs);    elems.clear();    while(!tmp.empty())    {        push(tmp.top());        tmp.pop();    }    return *this;}

关于两个版本的top()成员函数,一个是const的返回常量引用的,一个是没有这些常量修饰的,这本质上上是因为Stack模板类内部的CONT容器的back()函数(也即STL对所有容器类的一种要求或者说是规范,返回两个版本的返回容器内部元素,一个mutable sequence,另一个是nonmutable sequence

// deque,内部可变和不可变两个版本的back()成员函数typedef typename _Mybase::reference reference;typedef typename _Mybase::const_reference const_reference;reference back()    {   // return last element of mutable sequence    return (*(end() - 1));    }const_reference back() const    {   // return last element of nonmutable sequence    return (*(end() - 1));    }
0 0
原创粉丝点击