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

  • 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, empty
template<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));
    }

你可能感兴趣的:(C++,template,成员模板,模板的模板参数)