C++ 高階操作:模板元編程

2021-12-28 CPP開發者

(給CPP開發者加星標,提升C/C++技能)

https://www.cnblogs.com/xiangtingshen/p/11613183.html

【導讀】:泛型編程大家應該都很熟悉了,主要就是利用模板實現「安全的宏」,而模板元編程區別於我們所知道的泛型編程,它是一種較為複雜的模板,屬於C++的高階操作了,它最主要的優點就在於把計算過程提前到編譯期,能帶來可觀的性能提升。接下來,請和小編一起來學習吧。

以下是正文

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++ 模板可以做以下事情:編譯期數值計算、類型計算、代碼計算(如循環展開),其中數值計算實際意義不大,而類型計算和代碼計算可以使得代碼更加通用,更加易用,性能更好(也更難閱讀,更難調試,有時也會有代碼膨脹問題)。

編譯期計算在編譯過程中的位置請見下圖。

使用模板元編程的基本原則就是:將負載由運行時轉移到編譯時,同時保持原有的抽象層次。

其中負載可以分為兩類,一類就是程序運行本身的開銷,一類則是程式設計師需要編寫的代碼。

前者可以理解為編譯時優化,後者則是為提高代碼復用度,從而提高程式設計師的編程效率。

圖靈完備:

圖靈完備是對計算能力的描述。

簡單判定圖靈完備的方法就是看該語言能否模擬出圖靈機圖靈不完備的語言常見原因有循環或遞歸受限(無法寫不終止的程序,如 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判斷,參考如下代碼:

#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; }; 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';}

程序輸出結果: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();}

程序輸出結果:

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_tagoutput_iterator_tagforward_iterator_tagbidirectional_iterator_tagrandom_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++ 模板元編程的一點體會

- EOF -

關於 C++ 模板元編程,歡迎在評論中和我探討。覺得文章不錯,請點讚和在看支持我繼續分享好文。謝謝!

關注『CPP開發者』

看精選C++技術文章 . 加C++開發者專屬圈子

↓↓↓

點讚和在看就是最大的支持❤️

相關焦點

  • 淺談 C++ 元編程
    元編程作為一種新興的編程方式,受到了越來越多的廣泛關注。結合已有文獻和個人實踐,對有關 C++ 元編程進行了系統的分析。受限於 C++ 對模板本身的限制,Andrei Alexandrescu 等人又發明了 D 語言,把元編程提升為語言自身的一個特性。元編程已被廣泛的應用於現代 C++ 的程序設計中。由於元編程不同於一般的編程,在程序設計上更具有挑戰性,所以受到了許多學者和工程師的廣泛關注。1.4 元編程的語言支持C++ 的元編程主要依賴於語言提供的模板機制。
  • scratch/python/c++,小孩學編程學哪個好?
    編程貓是我國的一款優秀軟體,它模仿了scratch,並進行了大規模改進,但基本邏輯都是一樣的。編程貓的公司點貓科技獲得了多輪融資,並積極進行教材化操作,應該是很有前途的軟體。不過對於編程貓的積木設置,不少人有不同的看法。
  • C++模版的本質
    因為編譯器不知道數據類型,那些對void*指針進行偏移操作(算術操作)會非常危險(GNU支持),所以操作會特別小心,這個給實現增加了複雜度。所以要滿足通用(支持各種容器),設計複雜度低,效率高,類型安全的算法,模板函數就應運而生了,模板函數就是用來實現通用算法並滿足上面要求。
  • 九大程式語言優缺點第四期:c++
    上一期給大家介紹了C語言,理所應當的本期給大家了解下C語言的超集,c++那麼接下來給大家介紹主流程式語言:C++、JavaScript、C#、Ruby、PHP以及Objective-C,對於這幾種語言,大家都能看到由其打造的頂尖應用,我們一起來了解一下吧。
  • python+C、C++混合編程的應用
    有的語言專注於簡單高效,比如python,內建的list,dict結構比c/c++易用太多,但同樣為了安全、易用,語言也犧牲了部分性能。在有些領域,比如通信,性能很關鍵,但並不意味這個領域的coder只能苦苦掙扎於c/c++的陷阱中,比如可以使用多種語言混合編程。
  • 給小學六年級的簡易模板元「編程」入門-基礎編程
    並且本位內容比較淺顯,旨在提供一個利用模板進行編程的通用思路,進階的內容我會給一些參考資料一起學習交流。本文按計劃分為上下兩章,分別是基礎編程和技巧。基礎編程介紹了如何用模板做一些簡單的編程。技巧部分會說一些模板元編程的技法。程式語言和編譯時一個簡單的程式語言通常由什麼要素組成?這個問題很好回答,那就是數據和流程。
  • PCL | 深入理解C++模板元編程
    它的構建模式和STL是一樣的,是基於大量的模板構建的庫。  因此,對於模板編程來講,實現和聲明是不能實現分離式編譯的(不嚴格的說法),究其原因,還是模板參數T未知,我們使用模板時,一般只會include<*.h>文件,我們在當前cpp裡引用了.h文件,就可以特化.h文件的內容,但也只能特化.h內容,無法特化.h對應的那個cpp的內容。
  • 如何正確通過 C++ Primer 學習 C++?
    以C++ Primer第五版為例,第一遍讀的時候:Part1也就是前七章,除了6.6,6.7節,都要通讀。尤其是第三章初步介紹了vector和string,簡直就是新手福音,搞定這兩個容器就能寫一些簡單的程序。Part2基本就是數據結構和算法,如果有基礎讀起來很輕鬆。9,11兩章介紹的容器,以及12.1節的智能指針要通讀。
  • Scratch、Python、C++誰才是學習編程的第一選擇?
    在選擇讓孩子學習編程學習時,很多家長會對選擇什麼樣的程式語言糾結,相信下面這些疑惑,你肯定也有過:Scratch的那種拖塊看起來像玩遊戲不如 Python、C++ 這樣的代碼程式語言高級。那麼,家長應該如何選擇孩子的第一門程式語言呢?如果想系統地學習編程,基本的進階過程就是 Scratch→Python→c++。Scratch 是麻省理工學院開發的圖形化編程工具。這個軟體最大的特點就是:使用者可以不認識英文單詞,不會使用鍵盤,也可以編程。幾乎所有的孩子都會一眼喜歡上這個軟體,建立起做編程的興趣。
  • C++基礎總結(一):從「hello world」入門C++!
    C++ 是一種靜態類型的、編譯式的、通用的、大小寫敏感的、不規則的程式語言,支持過程化編程、面向對象編程和泛型編程。C++ 被認為是一種中級語言,它綜合了高級語言和低級語言的特點。C++ 是由 Bjarne Stroustrup 於 1979 年在新澤西州美利山貝爾實驗室開始設計開發的。
  • c++入門教程-1
    但是,C++的開發效率確實比C要高很多,所以我仍然採用斷章取義的方法來介紹C++的知識。不管是C還是C++,實用就好。一、C++程序的命名規則C++頭文件一般採用.h後綴,也用有.hpp的。1、安裝g++編譯器用root用戶登錄伺服器,執行以下命令安裝或升級gcc-c++編譯器。yum -y install gcc-c++如果您的CentOS系統沒有安裝gcc-c++,以上命令就會安裝最新版本的gcc-c++,如果已經安裝了gcc-c++,就會更新到最新版本的gcc-c++,所以,以上命令不管執行多少次都沒有問題。
  • C++typename的由來和用法
    本文轉載自【微信公眾號:羽林君,ID:Conscience_Remains】經微信公眾號授權轉載,如需轉載與原文作者聯繫前言在C++模板函數的使用過程中,我們經常可以看到一個typename的使用,例如這樣的操作但是除此之外
  • C++ Primer Plus中文版(第4、5、6版)
    C++ Primer Plus全書分17章和10個附錄,分別介紹了C++程序的運行方式、基本數據類型、複合數據類型、循環和關係表達式、分支語句和邏輯操作符、函數重載和函數模板、內存模型和名稱空間、類的設計和使用、多態、虛函數、動態內存分配、繼承、代碼重用、友元、異常處理技術、string類和標準模板庫、輸入/輸出等內容。
  • 現代C++函數式編程
    導讀: 本文作者從介紹函數式編程的概念入手,分析了函數式編程的表現形式和特性,最終通過現代C++的新特性以及一些模板雲技巧實現了一個非常靈活的pipeline
  • 跟我學C++中級篇——STL的學習
    一、c++標準庫C++的標準庫主要包含兩大類,首先是包含C的標準庫的,當然,為了適應c++對一些C庫進行了少許的修改和增加。最重要的當然是面向對象的c++庫;而c++庫又可以分成兩大類,即面向對象的c++庫和標準模板庫,也就是題目中的STL。
  • 2021年最新整理, C++ 學習資料,含C++ 新特性、入門教程、推薦書籍、優質文章、學習筆記、教學視頻等​
    :問題、方案和設計準則》《C++ Templates中文版》《C++設計新思維》《C++模板元編程》《C++ 語言的設計與演化》《深度探索C++ 對象模型》《泛型編程與STL》📰 文章推薦每個c++開發人員都應該使用的10個c++11特性
  • C++模板的介紹
    模板是泛型編程的基礎,泛型編程即以一種獨立於任何特定類型的方式編寫代碼。C++模板的作用,類似於C中的typedef,不過C++中模板有更多的靈活性。C++模板類型,類似於Objective-C中的泛型。C++通過類模板來實現泛型支持。2.C++中有兩種模板,分別是函數模板和類模板。
  • 不要再爭了,最有錢途的程式語言在這裡
    關於程式語言之爭,歷史真的是一直在重複上演,c 比 c++性能高,c++ 比 java 性能高,java 比 Ruby 性能高.」一直到現在的 Python「每一次新的程式語言出現,都會被守舊的人批評,而被新生力量熱捧。「爭來爭去有什麼意思,每一種程式語言的出現都是科學技術發展所推動的進步。
  • c++的輸入與輸出
    c++輸入與輸出C++ 標準庫提供了一組豐富的輸入/輸出功能,本章將討論 C++ 編程中最基本和最常見的 I/O 操作。輸入輸出並不是c++語言的正式組成成分,c和c++沒有為輸入輸出提供專門的結構。在c語言中輸入輸出是通過調用scanf和printf 實現的,在c++中是通過調用流對象cin和cout實現的。
  • 提高編程質量和效率使用UG模板技巧
    大家好,我是橘子,今天給大家分享的是關於UG編程實際案例.喜歡我分享的文章或者還需要別的學習資料可以關注我私信UG都可以免費領取提高編程質量和效率使用UG模板技巧 通過建立UG編程模板的方式,以提高我們用ug編程的工作質量和效率,並