https://blog.csdn.net/K346K346/article/details/82748163
https://www.jianshu.com/p/b56d59f77d53
https://www.cnblogs.com/my_life/articles/10132060.html
元編程側重點在於「用代碼生成代碼」,泛型編程側重點在於「減小代碼對特定數據類型的依賴」。
1.概述
模板元編程(Template Meta programming,TMP)是編寫生成或操縱程序的程序,也是一種復雜且功能強大的編程范式(Programming Paradigm)。
C++模板給C++提供了元編程的能力,但大部分用戶對 C++ 模板的使用並不是很頻繁,大致限於泛型編程,
在一些系統級的代碼,尤其是對通用性、性能要求極高的基礎庫(如 STL、Boost)幾乎不可避免在大量地使用 C++ 模板以及模板元編程。
模版元編程完全不同於普通的運行期程序,因為模版元程序的執行完全是在編譯期,並且模版元程序操縱的數據不能是運行時變量,只能是編譯期常量,不可修改。
另外它用到的語法元素也是相當有限,不能使用運行期的一些語法,比如if-else、for和while等語句都不能用。
因此,模版元編程需要很多技巧,常常需要類型重定義、枚舉常量、繼承、模板偏特化等方法來配合,因此模版元編程比較復雜也比較困難。
2.模板元編程的作用
C++ 模板最初是為實現泛型編程設計的,但人們發現模板的能力遠遠不止於那些設計的功能。
一個重要的理論結論就是:C++ 模板是圖靈完備的(Turing-complete),就是用 C++ 模板可以模擬圖靈機。
理論上說 C++ 模板可以執行任何計算任務,但實際上因為模板是編譯期計算,
其能力受到具體編譯器實現的限制(如遞歸嵌套深度,C++11 要求至少 1024,C++98 要求至少 17)。
C++ 模板元編程是“意外”功能,而不是設計的功能,這也是 C++ 模板元編程語法丑陋的根源。
C++ 模板是圖靈完備的,這使得 C++代碼存在兩層次,其中,執行編譯計算的代碼稱為靜態代碼(static code),執行運行期計算的代碼稱為動態代碼(dynamic code),
C++的靜態代碼由模板實現,編寫C++的靜態代碼,就是進行C++的模板元編程。
具體來說 C++ 模板可以做以下事情:編譯期數值計算、類型計算、代碼計算(如循環展開),其中數值計算實際意義不大,而類型計算和代碼計算可以使得代碼更加通用,更加易用,性能更好(也更難閱讀,更難調試,有時也會有代碼膨脹問題)。
編譯期計算在編譯過程中的位置請見下圖。
使用模板元編程的基本原則就是:將負載由運行時轉移到編譯時,同時保持原有的抽象層次。
其中負載可以分為兩類,一類就是程序運行本身的開銷,一類則是程序員需要編寫的代碼。
前者可以理解為編譯時優化,后者則是為提高代碼復用度,從而提高程序員的編程效率。
https://www.kancloud.cn/wizardforcel/beyond-stl/114986
boost中的泛 型編程與模板元編程
1
2
3
|
圖靈完備是對計算能力的描述。
簡單判定圖靈完備的方法就是看該語言能否模擬出圖靈機圖靈不完備的語言常見原因有循環或遞歸受限(無法寫不終止的程序,如
while
(
true
){}; ), 無法實現類似數組或列表這樣的數據結構(不能模擬紙帶). 這會使能寫的程序有限圖靈完備可能帶來壞處, 如C++的模板語言, 模板語言是在類型檢查時執行, 如果編譯器不加以檢查,我們完全可以寫出使得C++編譯器陷入死循環的程序.圖靈不完備也不是沒有意義, 有些場景我們需要限制語言本身. 如限制循環和遞歸, 可以保證該語言能寫的程序一定是終止的.
|
3.模板元編程的組成要素
從編程范式上來說,C++模板元編程是函數式編程,用遞歸形式實現循環結構的功能,用C++ 模板的特例化提供了條件判斷能力,這兩點使得其具有和普通語言一樣通用的能力(圖靈完備性)。
模版元程序由元數據和元函數組成,元數據就是元編程可以操作的數據,即C++編譯器在編譯期可以操作的數據。
元數據不是運行期變量,只能是編譯期常量,不能修改,常見的元數據有enum枚舉常量、靜態常量、基本類型和自定義類型等。
元函數是模板元編程中用於操作處理元數據的“構件”,可以在編譯期被“調用”,因為它的功能和形式 和 運行時的函數類似,而被稱為元函數,它是元編程中最重要的構件。
元函數實際上表現為C++的一個類、模板類或模板函數,它的通常形式如下:
template<int N, int M>
struct meta_func
{
static const int value = N+M;
}
調用元函數獲取value值:cout<<meta_func<1, 2>::value<<endl;
meta_func的執行過程是在編譯期完成的,實際執行程序時,是沒有計算動作而是直接使用編譯期的計算結果。元函數只處理元數據,元數據是編譯期常量和類型,所以下面的代碼是編譯不過的:
int i = 1, j = 2;
meta_func<i, j>::value; //錯誤,元函數無法處理運行時普通數據
模板元編程產生的源程序是在編譯期執行的程序,因此它首先要遵循C++和模板的語法,但是它操作的對象不是運行時普通的變量,因此不能使用運行時的C++關鍵字(如if、else、for),
可用的語法元素相當有限,最常用的是:
enum、static const //用來定義編譯期的整數常量;
typedef/using //用於定義元數據;[類型別名]
T/Args... //聲明元數據類型; 【模版參數:類型形參,非類型形參】
Template //主要用於定義元函數; 【模版類,特化,偏特化】
:: //域運算符,用於解析類型作用域獲取計算結果(元數據)。【獲取元數據,元類型】
實際上,模板元中的if-else可以通過type_traits來實現,它不僅僅可以在編譯期做判斷,還可以做計算、查詢、轉換和選擇。
模板元中的for等邏輯可以通過遞歸、重載、和模板特化(偏特化)等方法實現。
4.模板元編程的控制邏輯
第一個 C++ 模板元程序由Erwin Unruh 在 1994 年編寫,這個程序計算小於給定數 N 的全部素數(又叫質數),程序並不運行(都不能通過編譯),
而是讓編譯器在錯誤信息中顯示結果(直觀展現了是編譯期計算結果,C++ 模板元編程不是設計的功能,更像是在戲弄編譯器。從此,C++模板元編程的能力開始被人們認識到。
在模版元程序的具體實現時,由於其執行完全是在編譯期,所以不能使用運行期的一些語法,比如if-else、for和while等語句都不能用。這些控制邏輯需要通過特殊的方法來實現。
4.1 if判斷
模板元編程中實現條件if判斷,參考如下代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
#include <iostream>
template<
bool
c, typename Then, typename Else>
class
IF_ {};
//基礎類模版
template<typename Then, typename Else>
class
IF_<
true
, Then, Else> {
public
: typedef Then reType; };
//類模版的偏特化; 如果第一個模版非類型參數為true,IF_<true, Then, Else>::reType的值為模版的第二個類型參數Then
template<typename Then, typename Else>
class
IF_<
false
,Then, Else> {
public
: typedef Else reType; };
//類模版的偏特化
int
main()
{
const
int
len = 4;
// 定義一個指定字節數的類型
typedef
IF_<
sizeof
(
short
)==len,
short
,
IF_<
sizeof
(
int
)==len,
int
,
IF_<
sizeof
(
long
)==len,
long
,
IF_<
sizeof
(
long
long
)==len,
long
long
,
void
>::reType>::reType>::reType>::reType int_my;
std::cout <<
sizeof
(int_my) <<
'\n'
;
}
/*分析最里面的一層:
* IF_<sizeof(long long)==len, long long, void>::reType
* 如果sizeof(long long) == 4, 上面的表達式返回long long, 否則返回void
*/
|
程序輸出結果:4。
實際上,從C++11開始,可以通過type_traits來實現。因為type_traits提供了編譯期選擇特性:std::conditional,它在編譯期根據一個判斷式選擇兩個類型中的一個,和條件表達式的語義類似,類似於一個三元表達式。它的原型是:
template< bool B, class T, class F >
struct conditional;
所以上面的代碼可以改寫為如下代碼:
#include <iostream>
#include <type_traits>
int main()
{
const int len = 4;
// 定義一個指定字節數的類型
typedef
std::conditional<sizeof(short)==len, short,
std::conditional<sizeof(int)==len, int,
std::conditional<sizeof(long)==len, long,
std::conditional<sizeof(long long)==len, long long,
void>::type>::type>::type>::type int_my;
std::cout << sizeof(int_my) << '\n';
}
程序同樣編譯輸出4。
4.2循環展開
編譯期的循環展開( Loop Unrolling)可以通過模板特化來結束遞歸展開,達到運行期的for和while語句的功能。下面看一個編譯期數值計算的例子。
#include <iostream>
template<int N> class sum
{
public: static const int ret = sum<N-1>::ret + N;
};
template<> class sum<0>
{
public: static const int ret = 0;
};
int main()
{
std::cout << sum<5>::ret <<std::endl;
return 0;
}
程序輸出:15。
當編譯器遇到sumt<5>時,試圖實例化之,sumt<5> 引用了sumt<5-1>即sumt<4>,試圖實例化sumt<4>,以此類推,直到sumt<0>,sumt<0>匹配模板特例,sumt<0>::ret為 0,sumt<1>::ret為sumt<0>::ret+1為1,以此類推,sumt<5>::ret為15。
值得一提的是,雖然對用戶來說程序只是輸出了一個編譯期常量sumt<5>::ret,但在背后,編譯器其實至少處理了sumt<0>到sumt<5>共6個類型。
從這個例子我們也可以窺探 C++ 模板元編程的函數式編程范型,對比結構化求和程序:for(i=0,sum=0; i<=N; ++i) sum+=i; 用逐步改變存儲(即變量 sum)的方式來對計算過程進行編程,
模板元程序沒有可變的存儲(都是編譯期常量,是不可變的變量),要表達求和過程就要用很多個常量:sumt<0>::ret,sumt<1>::ret,...,sumt<5>::ret。
函數式編程看上去似乎效率低下(因為它和數學接近,而不是和硬件工作方式接近),但有自己的優勢:描述問題更加簡潔清晰,沒有可變的變量就沒有數據依賴,方便進行並行化。
4.3switch/case分支
同樣可以通過模板特化來模擬實現編譯期的switch/case分支功能。參考如下代碼:
#include <iostream>
using namespace std;
template<int v> class Case
{
public:
static inline void Run()
{
cout << "default case" << endl;
}
};
template<> class Case<1>
{
public:
static inline void Run()
{
cout << "case 1" << endl;
}
};
template<> class Case<2>
{
public:
static inline void Run()
{
cout << "case 2" << endl;
}
};
int main()
{
Case<2>::Run();
}
程序輸出結果:
case 2
5.特性、策略與標簽
利用迭代器,我們可以實現很多通用算法,迭代器在容器與算法之間搭建了一座橋梁。求和函數模板如下:
#include <iostream>
#include <vector>
template<typename iter>
typename iter::value_type mysum(iter begin, iter end)
{
typename iter::value_type sum(0);
for(iter i=begin; i!=end; ++i)
sum += *i;
return sum;
}
int main()
{
std::vector<int> v;
for(int i = 0; i<100; ++i)
v.push_back(i);v.push_back(i);
std::cout << mysum(v.begin(), v.end()) << '\n';
}
程序編譯輸出:4950。
我們想讓 mysum() 對指針參數也能工作,畢竟迭代器就是模擬指針,但指針沒有嵌套類型 value_type,可以定義 mysum() 對指針類型的特例,但更好的辦法是在函數參數和 value_type 之間多加一層特性(traits)。
template<typename iter>
class mytraits //標准容器通過這里獲取容器元素的類型
{
public: typedef typename iter::value_type value_type;
};
template<typename T>
class mytraits<T*> //數組類型的容器,通過這里獲取數組元素的類型
{
public: typedef T value_type;
};
template<typename iter>
typename mytraits<iter>::value_type mysum(iter begin, iter end)
{
typename mytraits<iter>::value_type sum(0);
for(iter i=begin; i!=end; ++i)
sum += *i;
return sum;
}
int main()
{
int v[4] = {1,2,3,4};
std::cout << mysum(v, v+4) << '\n';
return 0;
}
程序輸出:10。
其實,C++ 標准定義了類似的 traits, std::iterator_trait(另一個經典例子是 std::numeric_limits) 。
- traits特性對類型的信息(如 value_type、 reference)進行包裝,使得上層代碼可以以統一的接口訪問這些信息。
C++ 模板元編程會涉及大量的類型計算,很多時候要提取類型的信息(typedef、 常量值等),如果這些類型信息的訪問方式不一致(如上面的迭代器和指針),我們將不得不定義特例,這會導致大量重復代碼的出現(另一種代碼膨脹),而通過加一層特性可以很好的解決這一問題。
另外,特性不僅可以對類型的信息進行包裝,還可以提供更多信息,當然,因為加了一層,也帶來復雜性。特性是一種提供元信息的手段。
- 策略(policy)一般是一個類模板,典型的策略是 STL 容器的分配器(如std::vector<>,完整聲明是template<class T, class Alloc=allocator<T>> class vector;)(這個參數有默認參數,即默認存儲策略),
策略類將模板的經常變化的那一部分子功能塊集中起來作為模板參數,這樣模板便可以更為通用,這和特性的思想是類似的。
- 標簽(tag)一般是一個空類,其作用是作為一個獨一無二的類型名字用於標記一些東西,典型的例子是 STL 迭代器的五種類型的名字。
input_iterator_tag
output_iterator_tag
forward_iterator_tag
bidirectional_iterator_tag
random_access_iterator_tag
實際上,std::vector<int>::iterator::iterator_category就是random_access_iterator_tag, 可以使用type_traits的特性is_same來判斷類型是否相同。
#include <iostream>
#include <vector>
#include <type_traits>
int main()
{
std::cout << is_same<std::vector<int>::iterator::iterator_category, std::random_access_iterator_tag >::value << std::endl;
return 0;
}
程序輸出:1。
有了這樣的判斷,還可以根據判斷結果做更復雜的元編程邏輯(如一個算法以迭代器為參數,根據迭代器標簽進行特例化以對某種迭代器特殊處理)。標簽還可以用來分辨函數重載。
6.小結
C++模板元編程是圖靈完備的且是函數式編程,主要特點是代碼在編譯期執行,可用於編譯期數值計算,能夠獲得更有效率的運行碼。模板的使用,也提高了代碼泛化。與此同時,模板元編程也存一定缺點,主要有:
(1)模板元編程產生的代碼較為復雜,難易閱讀,可讀性較差;
(2)大量模板的使用,編譯時容易導致代碼膨脹,提高了編譯時間;
(3)對於C++來說,由於各編譯器的差異,大量依賴模板元編程(特別是最新形式的)的代碼可能會有移植性的問題。
所以,對於模板元編程,我們需要揚其長避其短,合理使用模板元編程。
參考文獻
[1]C++11模版元編程
[2]C++模板元編程(C++ template metaprogramming)
[3]c++模板元編程五:switch/case語句編譯時運行
[4]c++ 模板元編程的一點體會