模板

模板作为 C++ 支持的第四种范程,最初与类相同,只是为了减少代码的编写。但如今,C++ 的模板元编程( TMP )已经成为了一种 图灵完备 的语言。模板编程与普通的函数式范程和面向对象范程相同,同时具备条件判断、循环、多态的特点。模板具有以下特点:

  • 将程序从运行期提前到编译期

  • 隐式接口和编译期多态

  • 图灵完备的 TMP (Template Meta Programmer)

  • 需要注意的是, TMP 与普通的模板编程应当区分开,TMP 意味着代码在编译期执行完毕,运行期只负责输出结果;而普通的模板编程只是用来重用代码

  • 某些错误的代码可能依然能够通过编译,这是因为如果计算结果不通过执行代码就可以在编译期分析出来,那么代码实际上并不会执行[1]

模板介绍

模板由关键字 templatetypename 声明,与普通声明不同的是,模板声明的是类型而不是变量。从而达到 “一次声明,多类型使用” 的目的。将不必要的工作移交给编译器处理,从而大大减少了代码工作量。

一个典型的模板声明与使用如下:

template <typename T, typename N>
void output(T a, N b){
   cout<<a<<' '<<b<<endl;
}

int main() {
   output<int, double>(10, 10.f);
   output<int, const char*>(20, "hello, world");
   return 0;
}
在面向对象中,一般我们习惯将函数的声明和实现进行分离,而在模板中,则需要将函数的声明和实现写在同一个文件中

什么是谓词?

谓词就是返回值为真或者假的函数。STL 容器中经常会使用到谓词,用于模板参数。[2]

模板参数默认值

与函数参数的默认值相同,你可以为模板参数指定默认类型。但不同的是,模板参数的默认值只是用来帮助编译器推导参数类型,而且对参数右边的参数不做要求,比如:

template <typename T, typename M = int, typename N>
void output(T a, N b){
   cout<<a<<' '<<b<<endl;
}

第二个参数有默认值,而第三个参数无默认值,这是完全合法的。

模板参数推导

在某些情况下,编译器可以根据实参类型来自动推导参数类型,从而可以在尖括号内少写一部分参数:

  • 若模板参数出现在实参列表,则使用实参类型

  • 若模板参数未出现在实参列表,则使用默认值

  • 若模板参数既没有出现在实参列表,也没有默认值,则需要显式指出

  • 所有可推导的模板参数必须连续位于模板参数右边,否则可推导模板参数需要显式指出

函数的返回值对模板参数推导无用

模板的特化

模板本质上是对函数进行泛化,而在某些情况下可能需要对模板进行特化,以便函数的执行针对性更强

模板的特化有两种形式:

  1. 使用函数多态:适用于 不同类型之间 的特化

  2. 使用 template<>:适用于 相同类型,不同值 的特化

例如:

// 类型特化
template<typename T>
void output(T a){
   cout<<a<<endl;
}

void output(int a){
   cout<<"int: "<<a<<endl;
}
// 值特化
template<int i>
void output(){
   cout<<i<<endl;
}

template<>
void output<1>(){
   cout<<"[1]"<<endl;
}

类型特化实质上就是函数重载

模板匹配

隐式接口和编译期多态[1]

与面向对象不同的是,模板和泛型编程最重要的是隐式接口和编译期多态,而不是显式接口和运行时多态。

例如:

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

从表面上来看,类型 T 必须拥有:

  • size, normalize, swap 成员函数,拷贝构造函数,可用于 someNastyWidget 的 operator> 函数,则就是所谓的 隐式接口

  • 以不同的参数对函数进行调用将会导致编译器生成不同的函数,这就是所谓的 编译期多态

更近一步地,通过 typename 的第二种语法,我们可以拓展隐式接口的范围:[3]

template<typename IterT>
void workWithIterator(IterT iter){
   typename iterator_traits<IterT>::value_type temp(*iter);
   cout<<temp;
}

如上例,在 iterator_traits 中,为 iterator 定义了一下隐式接口。用于得到迭代器中储存的类型

struct iterator_traits<_Tp*>{
using value_type          = remove_cv_t<_Tp>;
using difference_type   = ptrdiff_t;
using pointer          = _Tp*;
using reference          = _Tp&;
};
在 C++ 20 中,可以通过 concepts 将模板参数需要满足的隐式接口进行抽离,而不是分散到整个函数中

条件判断和循环

要实现条件判断,可以通过模板的特化来做到:

首先实现相等判断:[4]

template <typename T1, typename T2>
struct IS_SAME{
   enum{ result = 0 };
};

template<typename T>
struct IS_SAME<T, T>{
   enum { result = 1};
};

现在,当用相同类型调用 IS_SAME 是,其 result = 1, 当用不同类型调用 IS_SAME 时,其 result = 0。

然后实现条件语句:

template<bool cond, typename Type1, typename Type2>
struct if_{
   typedef Type1 return_type;
};

template<typename Type1, typename Type2>
struct if_<false, Type1, Type2>{
   typedef Type2 return_type;
};

现在,当 if_ 的第一个参数为 true 是,返回第二个模板参数的类型,当 if_ 的第一个参数为 false 时,返回第三个模板参数的类型:

typename if_<
            IS_SAME<int, char>::result,
            int, char>::return_type a;

可见,模板中的条件判断实现的是类型判断和类型选择

与普通的循环不同的是,模板中的循环是通过对函数进行 递归 实现的:

template<int i>
void output() {
   output<i - 1>();
   cout << i << " ";
}

template<>
void output<1>(){
   cout<<1<<" ";
}

int main() {
   output<10>();
   return 0;
}

显然,由于需要在模板特化时指定具体值,模板循环只适用于同一种类型

可变长模板参数

C++11引入了名为 参数包 的可变长模板参数,其声明方式和限制如下:

  • \'…​ 开头的参数为参数包

  • 参数包必须位于模板参数末尾

下面提供了展开参数包的方法

  1. 使用递归在函数中展开实参参数包:

    template<typename T>
    void output(T a){
       cout<<a<<' ';
    }
    
    template<typename T, typename ...Args>
    void output(T a, Args ...args){
       cout<<a<<' ';
       output(args...);
    }
    与普通模板不同的是,带有变长模板参数的模板特化形式必须位于泛化形式上方
  2. 在初始化列表、基类列表、异常声明列表、异常捕获列表、属性列表、成员初始化列表展开参数包:[5][6]

template<typename ...Bases>
class Derived: public Bases...{ // 在基类列表展开模板参数
   Derived(const Bases... bases) : Bases(bases)...{ // 在基类初始化列表中展开
   }
};

template<class... Types>
struct count {
   static const std::size_t value = sizeof...(Types); // 使用 size...展开包
};

template<typename ...exceptions>
void func() throw(exceptions...){ // 动态异常展开 {cpp}17已废弃

}
  1. 通过折叠表达式展开参数包[7]

自 C++17,添加了折叠表达式用于更方便地展开参数包:

template<typename ...Args>
double sum(Args ...args){
   return (args += ...);
}

当然,你依然需要注意精度的问题。

函数返回值类型提取

在某些情况下,使用模板可能会导致精度丢失:

template<typename T>
T sum(T a){
   return a;
}

template<typename T, typename ...Args>
T sum(T a, Args ...args){
   return a + sum(args...);
}

现在我们通过一系列巧妙的设置,这样调用该函数:cout<<sum(1, 1.2);。注意,这里由于第一个参数是 int ,因此该序列求和的结果为 int 类型。这就导致了小数部位精度丢失。

现在,我们可以让编译器自己提取返回值类型:[8]

template <typename T1, typename ...Args>
struct higest_precision{
   typedef typename higest_precision<Args...>::type T2;
   // 将 nullptr 转换为两种类型的指针,然后再解引用
   typedef decltype(*((T1*)nullptr) + *((T2*) nullptr)) type;
};

template<typename T>
struct higest_precision<T>{
   typedef T type;
};

template<typename T>
T sum(T a){
   return a;
}

template<typename T, typename ...Args>
typename higest_precision<T, Args...>::type
sum(T a, Args ...args){
   return a + sum(args...);
}

对于没有使用参数包的模板来说,可以使用 ``auto + decltype`` 这种更加简单的方式:
template<typename T1, typename T2, typename T3>
auto sum(T1 a, T2 b, T3 c) -> decltype(a + b + c){
   return a + b + c;
}

这种方式被称为 函数后置返回类型

奇异递归模板

奇异递归模板 ( CRTP (Curiously Recurring Template Pattern) ) 使得父类可以在编译器感知到子类的存在。主要用于:

  • 静态多态

    在使用接口规范子类时,大量运行时的类型转换会导致极大的开销,使用 CRTP 可以在编译期就获取子类,通过 static_cast 获取子类指针就避免了 dynamic_cast 的开销 +

template <typename T>
class Bird{
public:
   T getBird(){
      // 静态多态
      return static_cast<T&>(*this);
   }
};

class Chicken: public Bird<Chicken>{

};

template<typename T>
const char* bird_name(Bird<T>& bird){
   return typeid(bird.getBird()).name();
}

int main(){
   Chicken ch;
   std::cout<<bird_name(ch);
   return 0;
}
  • 颠倒继承 (Upside Down Inheritance),就是通过父类向子类添加功能。因为它的效果与普通继承父到子的逻辑是相反的。enable_shared_from_this 就是利用了颠倒继承来实现所需要的功能的。[9]

完美转发

假设我们有个函数 factory,其功能是将传入的参数包装成了一个 shared_ptr。一个简单的实现如下:

template<typename T, typename Arg>
std::shared_ptr<T> factory(Arg arg) {
    return std::shared_ptr<T>(new T(arg));
}

但问题是 arg 会引起一次拷贝,为了性能考虑,可以将参数类型更改为 T&,但是这样就没法匹配到右值了。另一个方式是将参数类型更改为 const T&,但是这样函数内又没法对 arg 做处理了。

另一种方式是分别为左值和右值分别写一个函数。但是无疑十分麻烦。

完美转发就因此而生。完美转发的核心概念为引用折叠

引用折叠的规则可以简要概括为:

T& & -> T&
T&& & --> T&
T& && -> T&
T&& && -> T&&

值得注意的是 T&&,其完美保留了参数的实际类型,因此又被称为完美引用。完美引用可以同时匹配左值和右值。

借助完美引用和 std::forward,我们可以写出一个简单形式的函数:

template<typename T, typename Arg>
std::shared_ptr<T> factory(Arg&& arg) {
    return std::shared_ptr<T>(new T(std::forward<T>(arg)));
}

调用的方式为:

auto ptr  = factory<int>(10);
int  a    = 20;
auto ptr2 = factory<int>(a);

1. Effective C++ 第三版条款41:了解隐式接口和编译期多态
3. Effective C++ 第三版条款42:了解 typename 的双重意义
4. 深入实践 C++ 模板编程(温宇杰):13.2 元函数
6. ISO/IEC 14882 14.5.3 Variadic templates
8. 深入实践 C++ 模板编程(温宇杰):例 15.10
Last moify: 2023-12-15 04:28:35
Build time:2025-07-18 09:41:42
Powered By asphinx