【C++】深入理解模板

1、简介

模板是一种代码复用方式,其它的代码复用方式还包括继承和组合。当我们使用模板时,参数由编译器来替换,这非常像原来的宏方法,但却更清晰、更容易使用。在C++中,模板实现了参数化类型的概念,放在一对尖括号中,通过template这个关键字,告诉编译器随后的定义将操作一个或更多未指明的类型,当由这个模板产生实际代码时,必须指定这些类型以使编译器能够替换它们。下面是一个简单的模板类。

template <class T> // 模板类 T为未知类型 其它的为模板相关的关键字和固定格式
class Object
{
public:
    T getValue() const { return mValue; } // 内联定义
    void setValue(T value);
private:
    T mValue;
};
template <class T> // 注意添加template声明及参数列表Object<T>
void Object<T>::setValue(T value) // 非内联定义
{
    mValue = value;
}
int main()
{
    Object<int> a; // 编译器用int类型扩展模板类Object并进行实例化
    a.setValue(100);
    a.getValue();
    return 0;
}

即使是在创建非内联函数定义时,我们还是通常把模板的所有声明和定义都放入一个头文件中,这似乎违背了通常的头文件规则,即不要放置分配存储空间的任何东西,这条规则是为了防止在链接期间的多重定义错误,但模板定义很特殊,在templage声明之后的任何东西都意味着在当时不为它分配存储空间,而是一直处于等待状态直到被一个模板示例告知。在编译器和链接器有机制能去掉同一模板的多重定义,所以为了使用方便,几乎总是在头文件中放置全部的模板声明和定义。

2、函数模板

除了上面提到的类模板,模板还可以应用于函数,见下面的例子。

template <class T>
void foo(T t) {}
int main()
{
    foo(100);
    return 0;
}

后面还会对函数模板作个详细的介绍。

3、无类型模板参数

模板参数并不局限于类定义的类型,可以使用编译器内置类型,这些参数值在编译期间变成模板的特定示例的常量。在类模板中,可以为模板参数提供默认参数,但是在函数模板中却不行,见下面的例子。

template <class T = int, int size = 100>
class Array
{
    T array[size];
};
int main()
{
    Array<int, 10> a;
    Array<int> b;
    Array<> c;
    return 0;
}

尽管不能在函数模板中使用默认的模板参数,却能够用模板参数作为普通函数的默认参数,见下面的例子。

template <class T>
T sum(T *b, T *e, T init = T())
{
    while (b != e) init += *b++;
    return init;
}
int main()
{
    int a[] = {1, 2, 3};
    int total = sum(a, a + sizeof a / sizeof a[0]); // total为6 init默认值为0
    return 0;
}

4、模板类型的模板参数

模板可以接受另一个类模板作为模板参数类型,如果想在代码中将一个模板类参数用作另一个模板,编译器首先需要知道这个参数是一个模板,见下面的例子。

template <class T>
class Array {};
template <class T, template<class> class Seq>
// template <class T, template<class U> class Seq> // U可省略
class Container
{
    Seq<T> seq;
};
int main()
{
    Conatiner<int, Array> container;
    return 0;
}

对于模板参数中的默认参数,必须在模板类作为模板参数类型时重复声明默认参数,见下面的例子,参数名同样不是必须的。

template <class T, int N = 100>
class Array {};
template <class T, template<class, int = 100> class Seq>
class Container
{
    Seq<T> seq;
};

5、typename与template

typename有两种作用,一是替代模板参数中的关键字class,二是声明模板参数类型中的嵌套类型,否则这个类型不能被识别,见下面的例子。

template <typename T>
class A
{
    typename T::Type type_t;
    type_t t; // 等同于typename T::Type t;
};

另外,在模板代码中,template关键字也能起到类似typename关键字的作用,用于提示编译器后面的符号为模板中的符号,例如尖括号解析为模板符号,而不是大于小于,见下面的例子。

template <class T, class traits, class Allocator>
basic_string<T, traits, Allocator> to_string() const;

template <class T, int size>
basic_string<T> bitsetToString(const bitset<size> &bs)
{
    return bs. template to_string<T, char_traits<T>, allocator<T> >();
}

6、成员模板

成员模板包括成员函数模板和成员类模板,在类中声明template,见下面的例子。

template<typename T>
class complext
{
public:
    template<class X> complex(const complex<X>&);
};

template<typename T>
template<class X>
complex<T>::complex(const complex<X> &r) {}

complex float x(1.2);
complex double y(x); // y的T为double y的X为float

template<class T>
class Outer
{
public:
    template<class R>
    class Inner
    {
    public:
        void foo();
    };
};

template<class T>
template<class R>
void Outer<T>::Inner<R>::void() {}

7、函数模板参数

函数模板可以像类模板一样,使用尖括号进行声明,见下面的例子,使用int类型进行特化。

template<typename T>
cont T& min(const T& a, const T& b)
{
    return (a>b) ? b : a;
}
int x = min<int>(i, j);

不使用尖括号时,可以让编译器推断出参数类型,见下面的例子,如果两个参数的类型相同,自然没有问题,但是对于一个由模板参数来限定类型的函数参数,C++系统不能提供标准转换,所以如果两个参数的类型不同时将出错,比如说一个int类型一个double类型,不能自动进行类型转换,一种解决方法是使用尖括号帮助类型转换,是int转换为double,另一种解决方法是干脆提供两个不同的模板参数,但此时的函数返回类型是个问题,不知道应该返回哪个类型是合适的。

int x = min(i, j);
int x = min<double>(i, j);

template<typename T, typename U>
cont T& min(const T& a, const U& b)
{
    return (a>b) ? b : a;
}

若一个函数模板的返回类型是一个独立的模板参数,当调用它的时候就一定要明确指定它的类型,因为这时已经无法从函数参数中推断它的类型了,见下面的例子。

template<typename T>
T fromString(const std::string& s)
{
    std::istringstream is(s);
    T t;
    is >> t;
    return t;
}
int i = fromString<int>(std::string("1234"));

结合上面提到的对函数模板的说明,如果有一个函数模板,它的模板参数既作为参数类型又作为返回类型,那么一定要首先声明函数的返回类型参数,否则就不能省略掉函数参数表中的任何类型参数,见下面的例子。

template<typename R, typename P>
R implicit_cast(cont R& p)
{
    return p;
}
int i = 1;
float f = implicit_cast<float>(i);
int j = implicit_cast<int>(f);
char *p = implicit_cast<char*>(i); // error 标准不支持这种转换

函数模板还可以取地址作为函数指针,传递给另一个函数作为参数,见下面的例子,g的参数为f时需至少指定它们中的一个的类型T,剩下的由编译器推断。

template<typename T> void f(T*) {}
template<typename T> void g(void (*pf)(T*)) {}
void h(void (*pf)(int*){) {}
int main()
{
    h(&f); // 编译器推断类型T为int
    h(&f<int>); // 明确指定T为int
    g<int>(&f<int>); 明确指定g和f的T为int
    g(&f<int>); 明确指定f的T为int 编译器推断g的T为int
    g<int>(&f); 明确指定g的T为int 编译器推断f的T为int
    return 0;
}

8、函数模板重载

函数模板可以像普通函数一样,用相同的函数名进行重载,见下面的例子,编译器会选择一个最佳匹配函数,首先是强制调用的模板函数,然后是没有进行任何类型转换的准确匹配的普通函数,然后是模板,最后是需要进行类型转换的普通函数。

template<typename T>
const T& min(const T& a, cont T& b) { return a > b ? b : a; }
template<typename T>
const T& min(const T& a, cont T& b, const T& c);
const char* min(const char* a, const char* b) { return (<strcmp(a, b) < 0) ? a : b; }
double min(double a, double b) { return a < b ? a : b; }
int main()
{
    const char* s2 = "say", s1 = "knights";
    min(1, 2); // template 没有template时将调用double版的min
    min(1,0, 2,0); // double 准确匹配
    min(1, 2.0); // double 1转换为1.0
    min(s1, s2); // const char*  准确匹配
    min<>(s1, s2); // template 尖括号强制使用template
    return 0;
}

对于重载的多个函数模板,当它们都满足调用请求时,编译器会选择一个模板特化度高的版本,见下面的例子,任何类型都可以匹配第一个模板,第二个模板比第一个模板的特化程度更高,因为只有指针类型才能够匹配它,第三个模板特化程度最高,仅仅能被指向const的指针匹配调用,如果特化程度不能进行区别对待,将带来二义性,编译器会报错,这种特征称为半有序,稍后还会详细介绍模板特化的内容。

template<class T> void f(T);
template<class T> void f(T*);
template<class T> void f(const T*);
int main()
{
    f(0); // T
    int i = 0;
    f(&i); // T*
    const int j = 0;
    f(&j); // const T*
    return 0;
}

9、模板特化

模板特化指的是指定模板参数的类型,包括显式指定的类型和编译器推断出来的类型,显式特化见下面的例子,用指定的类型替换T,并且注意尖括号中的内容。

template<class T>
const T& min(const T& a, const T& b) {}
template<>
const char* const& min<const char*>(const char* const& a, const char* const& b) {}

在标准库中也有显式特化的例子,如下。

template<class T, class Allocator = allocator<T> >
class vector {};
template<>
class vector<bool, allocator<bool> > {};

类模板也可以半特化,这意味着在模板特化的某些的方法中至少还有一个方法,其模板参数是开放的,上面例子的模板特化vector限定了T为bool类型,但没有指定参数allocator的类型,如下的例子是个半特化。

template<class Allocator>
class vector<bool, Allocator>;

上面提到了函数模板的半有序,同样,类模板也有半有序,见下面的例子,后面几个的用法中出现了二义性,因为找到了多个匹配的模板特化版本。

template<class T, class U>
class C { public: void foo(); }; // 无特化
template<class U>
class C<int , U> { public: void foo(); }; // T=int
template<class T>
class C<T, double> { public: void foo(); }; // U=double
template<class T, class U>
class C<T*, U> { public: void foo(); }; // T*
template<class T, class U>
class C<T, U*> { public: void foo(); }; // U*
template<class T, class U>
class C<T*, U*> { public: void foo(); }; // T* U*
template<class T>
class C<T, T> { public: void foo(); }; // U=T
void test()
{
    C<float, int>().foo(); // 无特化
    C<int, float>().foo(); // T=int
    C<float, doble>().foo(); // U=double
    C<float, float>().foo(); // U=T
    C<float*, float>().foo(); // T*
    C<float, float*>().foo(); // U*
    C<float*, int*>().foo(); // T* U*
    C<int, int>().foo(); // error 二义性
    C<double, double>().foo(); // error 二义性
    C<float*, float*>().foo(); // error 二义性
    C<int, int*>().foo(); // error 二义性
    C<int*, int*>().foo(); // error 二义性
}

10、模板和友元

在类模板中声明一个友元函数模板,见下面的例子,使用了前置声明,friend声明的foo使用了尖括号,这告诉编译器foo是个函数模板,模板参数依赖于类模板的参数T,当然也可以使用一个独立的不依赖于类模板参数的模板参数。

template<class T> class Friendly;
template<class T> void foo(const Friendly<T>&);
template<class T> class Friendly
{
    friend void foo<>(const Friendly<T>&);
};
template<class T>
void foo(const Friendly<T> &r) {}

11、模板和继承

见下面的例子,类Base定义了一个静态成员变量count,用于记录实例化的个数,构造函数中加1,拷贝构造函数中加1,析构函数中减1,类Child和类Child2是类Base的两个子类,然后对Child和Child2分别实例化时,发现它们共享一个静态成员变量count,这是合理的,因为它们共用一个基类,那么,每个子类独自使用一个静态成员变量count可以吗?

class Base
{
public:
    Base() { ++count; }
    Base(const Base&) { ++count; }
    ~Base() { -- count; }
    static int count;
};
int Base::count = 0;
class Child : public Base {};
class Child2: public Base {};
int main()
{
    Child c; // count=1
    Child c2; // count=2
    Child2 c3; // count=3
    return 0;
}

接着上面的问题,每个子类独自使用一个静态成员变量count是可以的,方法是使用模板,见下面的例子,因为基类为模板,所以,基类对模板参数进行了扩展,所有的子类实际上都是派生于不同的基类。

template<class T>
class Base
{
public:
    Base() { ++count; }
    Base(const Base<T>&) { ++count; }
    ~Base() { -- count; }
    static int count;
};
template<class T>
int Base<T>::count = 0;
class Child : public Base<Child> {};
class Child2: public Base<Child2> {};
int main()
{
    Child c; // count=1
    Child c2; // count=2
    Child2 c3; // count=1
    return 0;
}

12、模板元程序

模板元程序就是编译时编程,见下面的例子,用于求解斐波那契数列,是一个递归模板,模板特化提高了终止递归的条件,利用了模板的特性,所有结果都是在编译时完成的。

template<int n>
struct Fib
{
    enum { val = Fib<n - 1>::val + Fib<n - 2>::val };
};
template<>
struct Fib<1> { enum { val = 1 }; };
template<>
struct Fib<0> { enum { val = 0 }; };

13、模板编译

一般将模板的完整定义放在一个独立的头文件中,而普通函数的定义与它们的声明一般是分离的,分别放于源文件和头文件中,这样做可能是基于下面的原因。
1)头文件中的非内联函数体会导致函数多重定义,在链接时产生错误。
2)隐藏实现。
3)头文件越小,编译时间就越短。

对于模板来说,模板本质上不是代码,而是产生代码的指令,只有模板的实例化才是真正的代码。当一个编译器在编译期间已经看到了一个完整的模板定义,又在同一个翻译单元内碰到了这个模板实例化点的时候,它就必须涉及这样一个事实,一个相同的实例化点可能出现另一个翻译单元内,处理这种情况最普遍的方法,是在每一个翻译单元内都为这个实例化生成代码,让链接器清除这些副本,另一种特殊的方法也可以很好地处理这种情况,就是用不能被内联的内联函数和虚函数表,具体实现方式由编译器而定,这种模板编译方式称为模板的包含模型。包含模型的缺点是暴露了所有的代码实现,头文件比函数体分开编译时大多了,相比传统编译模型而言,大大增加了编译时间。

为了帮助减少包含模型所需要的大的头文件,C++提供了两种代码组织机制,使得模板可以将声明与实现分离,一种是显式实例化,手工实例化每一个模板特化,一种是使用导出模板,export关键字,支持最大限度的独立的编译。显式实例化见下面的例子,不使用显示实例化即min_instance.cpp时,由于模板的声明与实现分离,链接器不能找到min的int和double特化版本,将出错,解决办法是使用min_instance.cpp中的显式实例化方式,为了手工实例化一个特定的模板特化,可以在该特化的声明前使用template关键字,min_instance.cpp包含了min.cpp而非min.h是因为编译器需要用模板定义来进行实例化。

// min.h
#inndef MIN_H
#define MIN_H
template<typename T>
const T& min(const T&, const T&);
#endif

// min.cpp
#inndef MIN_CPP
#define MIN_CPP
#include "min.h"
template<typename T>
const T& min(const T& a, const T& b)
{
    return (a > b) ? b : a;
}
#endif

// min_instance.cpp
#include "min.cpp"
tempalte const int& min(const int&, const int&);
tempalte const double& min(const double&, const double&);

// min_int.cpp
#include "min.h"
void testmin_int()
{
    min(1, 2);
}

// min_double.cpp
#include "min.h"
void testmin_double()
{
    min(1.1, 2.2);
}

// main.cpp
void testmin_int();
void testmin_double();
int main()
{
    testmin_int();
    testmin_double();
    return 0;
}

我们还可以手工实例化类和静态成员变量,当显式实例化一个类时,除了一些之前可能已经显式实例化了的成员外,特化所需要的所有成员函数都要进行实例化,与隐式实例化相比,隐式实例化只有被调用的成员函数才进行实例化。

前面介绍了显式实例化,下面介绍导出模板。导出模板并不常见,可能只有部分编译器支持,使用export关键字,见下面的例子。

// min.h
#inndef MIN_H
#define MIN_H
export template<typename T>
const T& min(const T&, const T&);
#endif

// min.cpp
#include "min.h"
export template<typename T>
const T& min(const T& a, const T& b)
{
    return (a > b) ? b : a;
}

结束

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值