1. 程式人生 > >c++ 可變引數模板展開原理

c++ 可變引數模板展開原理

原文連結:http://www.cnblogs.com/chengyuanchun/p/5757823.html
例子內容出自:祁宇《深入應用C++11程式碼遊湖與工程級應用》

1.概述

  C++11的新特性--可變模版引數(variadic templates)是C++11新增的最強大的特性之一,它對引數進行了高度泛化,它能表示0到任意個數、任意型別的引數。相比C++98/03,類模版和函式模版中只能含固定數量的模版引數,可變模版引數無疑是一個巨大的改進。然而由於可變模版引數比較抽象,使用起來需要一定的技巧,所以它也是C++11中最難理解和掌握的特性之一。

  雖然掌握可變模版引數有一定難度,但是它卻是C++11中最有意思的一個特性,本文希望帶領讀者由淺入深的認識和掌握這一特性,同時也會通過一些例項來展示可變引數模版的一些用法。

2.可變模版引數的展開

  可變引數模板和普通模板的語義是一樣的,只是寫法上稍有區別,宣告可變引數模板時需要在typename或class後面帶上省略號“...”。比如我們常常這樣宣告一個可變模版引數:template<typename...>或者template<class...>,一個典型的可變模版引數的定義是這樣的:

  template<typename... T> void fun(T... args) { }

上面的可變模版引數的定義當中,省略號的作用有兩個:

1.宣告一個引數包T... args,這個引數包中可以包含0到任意個模板引數;
2.在模板定義的右邊,可以將引數包展開成一個一個獨立的引數。

  上面的引數args前面有省略號,所以它就是一個可變模版引數,我們把帶省略號的引數稱為“引數包”,它裡面包含了0到N(N>=0)個模版引數。我們無法直接獲取引數包args中的每個引數的,只能通過展開引數包的方式來獲取引數包中的每個引數,這是使用可變模版引數的一個主要特點,也是最大的難點,即如何展開可變模版引數。

  可變模版引數和普通的模版引數語義是一致的,所以可以應用於函式和類,即可變模版引數函式和可變模版引數類,然而,模版函式不支援偏特化,所以可變模版引數函式和可變模版引數類展開可變模版引數的方法還不盡相同,下面我們來分別看看他們展開可變模版引數的方法。

3.可變模板引數函式

一個簡單的可變模版引數函式:

複製程式碼
template<typename... T>
void fun(T... args)
{
    // 列印引數個數
    cout << sizeof...(args) << endl;  
}
複製程式碼

上面的例子中,fun()沒有傳入引數,所以引數包為空,輸出的size為0,後面兩次呼叫分別傳入兩個和三個引數,故輸出的size分別為2和3。由於可變模版引數的型別和個數是不固定的,所以我們可以傳任意型別和個數的引數給函式f。這個例子只是簡單的將可變模版引數的個數打印出來,如果我們需要將引數包中的每個引數打印出來的話就需要通過一些方法了。展開可變模版引數函式的方法一般有兩種:一種是通過遞迴函式來展開引數包,另外一種是通過逗號表示式來展開引數包。下面來看看如何用這兩種方法來展開引數包。

3.1遞迴函式方式展開引數包

通過遞迴函式展開引數包,需要提供一個引數包展開的函式和一個遞迴終止函式,遞迴終止函式正是用來終止遞迴的,來看看下面的例子。

複製程式碼
#include <iostream>
using namespace std;

// 最終遞迴函式
void print()
{
  cout << "empty" << endl;  
}

// 展開函式
template <typename T, typename... Args>
void print(T head, Args... args)
{
    cout << head << ",";
    print(args...);      
}

int main()
{
    print(1, 2, 3, 4);
    return 0;    
}
複製程式碼

上例會輸出每一個引數,直到為空時輸出empty。展開引數包的函式有兩個,一個是遞迴函式,另外一個是遞迴終止函式,引數包Args...在展開的過程中遞迴呼叫自己,每呼叫一次引數包中的引數就會少一個,直到所有的引數都展開為止,當沒有引數時,則呼叫非模板函式print終止遞迴過程。

遞迴呼叫的過程是這樣的:

print(1,2,3,4);print(2,3,4);print(3,4);print(4);print();

上面的遞迴終止函式還可以寫成這樣:

template <class T>void print(T t){   cout << t << endl;}

修改遞迴終止函式後,上例中的呼叫過程是這樣的:

print(1,2,3,4);print(2,3,4);print(3,4);print(4);

當引數包展開到最後一個引數時遞迴為止。

比如再加一個:

//最終遞迴函式
void print() { cout << "empty" << endl; }

template <class T>
void print(T t){ cout << t<< endl;}

template <class T>
void print(T t, T t1){ cout << t<<"****" << t1<< endl;}

// 展開函式
template <typename T, typename... Args>
void print(T head, Args... args)
{
    cout << head << ",";
    print(args...);
}

int main()
{
    print(1, 2, 3, 4);
    return 0;
}
上面的呼叫順序是print(1,2,3,4);print(2,3,4);print(3,4);