<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          淺談 C++ 元編程

          共 20695字,需瀏覽 42分鐘

           ·

          2021-06-18 09:27


          置頂/星標(biāo)公眾號??,硬核文章第一時間送達(dá)!

          隨著 C++ 11/14/17 標(biāo)準(zhǔn)的不斷更新,C++ 語言得到了極大的完善和補充。元編程作為一種新興的編程方式,受到了越來越多的廣泛關(guān)注。結(jié)合已有文獻(xiàn)和個人實踐,對有關(guān) C++ 元編程進行了系統(tǒng)的分析。首先介紹了 C++ 元編程中的相關(guān)概念和背景,然后利用科學(xué)的方法分析了元編程的 演算規(guī)則基本應(yīng)用 和實踐過程中的 主要難點,最后提出了對 C++ 元編程發(fā)展的 展望


          1. 引言

          1.1 什么是元編程

          元編程 (metaprogramming) 通過操作 程序?qū)嶓w (program entity),在 編譯時 (compile time) 計算出 運行時 (runtime) 需要的常數(shù)、類型、代碼的方法。

          一般的編程是通過直接編寫 程序 (program),通過編譯器 編譯 (compile),產(chǎn)生目標(biāo)代碼,并用于 運行時 執(zhí)行。與普通的編程不同,元編程則是借助語言提供的 模板 (template) 機制,通過編譯器 推導(dǎo) (deduce),在 編譯時 生成程序。元編程經(jīng)過編譯器推導(dǎo)得到的程序,再進一步通過編譯器編譯,產(chǎn)生最終的目標(biāo)代碼。在使用 if 進行編譯時測試中,用一個例子說明了兩者的區(qū)別。

          因此,元編程又被成為 兩級編程 (two-level programming)生成式編程 (generative programming) 或 模板元編程 (template metaprogramming)

          1.2 元編程在 C++ 中的位置

          C++ 語言 = C 語言的超集 + 抽象機制 + 標(biāo)準(zhǔn)庫

          C++ 的 抽象機制 (abstraction mechanisms) 主要有兩種:面向?qū)ο缶幊?/strong> (object-oriented programming) 和 模板編程 (generic programming)

          為了實現(xiàn)面向?qū)ο缶幊蹋珻++ 提供了  (class),用 C++ 的已有 類型 (type) 構(gòu)造出新的類型。而在模板編程方面,C++ 提供了 模板 (template),以一種直觀的方式表示 通用概念 (general concept)

          模板編程的應(yīng)用主要有兩種:泛型編程 (generic programming) 和 元編程 (meta-programming)。前者注重于 通用概念 的抽象,設(shè)計通用的 類型 或 算法 (algorithm),不需要過于關(guān)心編譯器如何生成具體的代碼;而后者注重于設(shè)計模板推導(dǎo)時的 選擇 (selection) 和 迭代 (iteration),通過模板技巧設(shè)計程序。

          1.3 C++ 元編程的歷史

          1988 年,David R. Musser 和 Alexander A. Stepanov 提出了 模板 ,并最早應(yīng)用于 C++ 語言。Alexander A. Stepanov 等人在 Bjarne Stroustrup 的邀請下,參與了 C++ 標(biāo)準(zhǔn)模板庫 (C++ Standard Template Library, C++ STL) (屬于 C++ 標(biāo)準(zhǔn)庫 的一部分) 的設(shè)計。模板的設(shè)計初衷僅是用于泛型編程,對數(shù)據(jù)結(jié)構(gòu)和算法進行 抽象 (abstraction)

          而在現(xiàn)代 C++ 的時代,人們發(fā)現(xiàn)模板可以用于元編程。1994 年的 C++ 標(biāo)準(zhǔn)委員會會議上,Erwin Unruh 演示了一段利用編譯器錯誤信息計算素數(shù)的代碼。1995 年的 Todd Veldhuizen 在 C++ Report 上,首次提出了 C++ 模板元編程 的概念,并指出了其在數(shù)值計算上的應(yīng)用前景。隨后,Andrei Alexandrescu 提出了除了數(shù)值計算之外的元編程應(yīng)用,并設(shè)計了一個通用的 C++ 的模板元編程庫 —— Loki。受限于 C++ 對模板本身的限制,Andrei Alexandrescu 等人又發(fā)明了 D 語言,把元編程提升為語言自身的一個特性。

          元編程已被廣泛的應(yīng)用于現(xiàn)代 C++ 的程序設(shè)計中。由于元編程不同于一般的編程,在程序設(shè)計上更具有挑戰(zhàn)性,所以受到了許多學(xué)者和工程師的廣泛關(guān)注。

          1.4 元編程的語言支持

          C++ 的元編程主要依賴于語言提供的模板機制。除了模板,現(xiàn)代 C++ 還允許使用 constexpr 函數(shù)進行常量計算。由于 constexpr 函數(shù)功能有限,所以目前的元編程程序主要基于模板。這一部分主要總結(jié) C++ 模板機制相關(guān)的語言基礎(chǔ),包括 狹義的模板 和 泛型 lambda 表達(dá)式

          1.4.1 狹義的模板

          目前最新的 C++ 將模板分成了 4 類:類模板 (class template)函數(shù)模板 (function template)別名模板 (alias template) 和 變量模板 (variable template)。前兩者能產(chǎn)生新的類型,屬于 類型構(gòu)造器 (type constructor);而后兩者僅是語言提供的簡化記法,屬于 語法糖 (syntactic sugar)

          類模板 和 函數(shù)模板 分別用于定義具有相似功能的  和 函數(shù) (function),是泛型中對 類型 和 算法 的抽象。在標(biāo)準(zhǔn)庫中,容器 (container) 和 函數(shù) 都是 類模板 和 函數(shù)模板 的應(yīng)用。

          別名模板 和 變量模板 分別在 C++ 11 和 C++ 14 引入,分別提供了具有模板特性的 類型別名 (type alias) 和 常量 (constant) 的簡記方法。前者只能用于簡記 已知類型,并不產(chǎn)生新的類型;后者則可以通過 函數(shù)模板返回值 等方法實現(xiàn)。盡管這兩類模板不是必須的,但可以增加程序的可讀性(復(fù)雜性)。例如,C++ 14 中的 別名模板 std::enable_if_t<T> 等價于 typename std::enable_if<T>::type

          C++ 中的 模板參數(shù) (template parameter / argument) 可以分為三種:值參數(shù),類型參數(shù),模板參數(shù)。從 C++ 11 開始,C++ 支持了 變長模板 (variadic template):模板參數(shù)的個數(shù)可以不確定,變長參數(shù)折疊為一個 參數(shù)包 (parameter pack) ,使用時通過編譯時迭代,遍歷各個參數(shù)([sec|變長模板的迭代])。標(biāo)準(zhǔn)庫中的 元組 (tuple) —— std::tuple 就是變長模板的一個應(yīng)用(元組的 類型參數(shù) 是不定長的,可以用 template<typename... Ts> 匹配)。

          盡管 模板參數(shù) 也可以當(dāng)作一般的 類型參數(shù) 進行傳遞(模板也是一個類型),但之所以單獨提出來,是因為它可以實現(xiàn)對傳入模板的參數(shù)匹配。類型推導(dǎo)的例子(代碼)使用 std::tuple 作為參數(shù),然后通過匹配的方法,提取 std::tuple 內(nèi)部的變長參數(shù)。

          特化 (specialization) 類似于函數(shù)的 重載 (overload),即給出 全部模板參數(shù)取值(完全特化)或 部分模板參數(shù)取值(部分特化)的模板實現(xiàn)。實例化 (instantiation) 類似于函數(shù)的 綁定 (binding),是編譯器根據(jù)參數(shù)的個數(shù)和類型,判斷使用哪個重載的過程。由于函數(shù)和模板的重載具有相似性,所以他們的參數(shù) 重載規(guī)則 (overloading rule) 也是相似的。

          1.4.2 泛型 lambda 表達(dá)式

          由于 C++ 不允許在函數(shù)內(nèi)定義模板,有時候為了實現(xiàn)函數(shù)內(nèi)的局部特殊功能,需要在函數(shù)外專門定義一個模板。一方面,這導(dǎo)致了代碼結(jié)構(gòu)松散,不易于維護;另一方面,使用模板時,需要傳遞特定的 上下文 (context),不易于復(fù)用。(類似于 C 語言里的回調(diào)機制,不能在函數(shù)內(nèi)定義回調(diào)函數(shù),需要通過參數(shù)傳遞上下文。)

          為此,C++ 14 引入了 泛型 lambda 表達(dá)式 (generic lambda expression) :一方面,能像 C++ 11 引入的 lambda 表達(dá)式一樣,在函數(shù)內(nèi)構(gòu)造 閉包 (closure),避免在 函數(shù)外定義 函數(shù)內(nèi)使用 的局部功能;另一方面,能實現(xiàn) 函數(shù)模板 的功能,允許傳遞任意類型的參數(shù)。


          2. 元編程的基本演算

          C++ 的模板機制僅僅提供了 純函數(shù) (pure functional) 的方法,即不支持變量,且所有的推導(dǎo)必須在編譯時完成。但是 C++ 中提供的模板是 圖靈完備 (turing complete) 的,所以可以使用模板實現(xiàn)完整的元編程。

          元編程的基本 演算規(guī)則 (calculus rule) 有兩種:編譯時測試 (compile-time test) 和 編譯時迭代 (compile-time iteration) ,分別實現(xiàn)了 控制結(jié)構(gòu) (control structure) 中的 選擇 (selection) 和 迭代 (iteration)。基于這兩種基本的演算方法,可以完成更復(fù)雜的演算。

          2.1 編譯時測試

          編譯時測試 相當(dāng)于面向過程編程中的 選擇語句 (selection statement),可以實現(xiàn) if-else / switch 的選擇邏輯。

          在 C++ 17 之前,編譯時測試是通過模板的 實例化 和 特化 實現(xiàn)的 —— 每次找到最特殊的模板進行匹配;而 C++ 17 提出了使用 constexpr-if 的編譯時測試方法。

          2.1.1 測試表達(dá)式

          類似于 靜態(tài)斷言 (static assert),編譯時測試的對象是 常量表達(dá)式 (constexpr),即編譯時能得出結(jié)果的表達(dá)式。以不同的常量表達(dá)式作為參數(shù),可以構(gòu)造各種需要的模板重載。例如,代碼演示了如何構(gòu)造 謂詞 (predicate) isZero<Val>,編譯時判斷 Val 是不是 0

          template <unsigned Val> struct _isZero {
              constexpr static bool value = false;
          };
          template <> struct _isZero <0> {
              constexpr static bool value = true;
          };

          template <unsigned Val>
          constexpr bool isZero = _isZero<Val>::value;

          static_assert (!isZero<1>, "compile error");
          static_assert (isZero<0>, "compile error");

          代碼編譯時測試表達(dá)式

          2.1.2 測試類型

          在元編程的很多應(yīng)用場景中,需要對類型進行測試,即對不同的類型實現(xiàn)不同的功能。而常見的測試類型又分為兩種:判斷一個類型 是否為特定的類型 和 是否滿足某些條件。前者可以通過對模板的 特化 直接實現(xiàn);后者既能通過 替換失敗不是錯誤 SFINAE (Substitution Failure Is Not An Error) 規(guī)則進行最優(yōu)匹配,又能通過 標(biāo)簽派發(fā) (tag dispatch) 匹配可枚舉的有限情況。

          為了更好的支持 SFINAE,C++ 11 的 <type_traits> 除了提供類型檢查的謂詞模板 is_*/has_*,還提供了兩個重要的輔助模板:

          • std::enable_if 將對條件的判斷 轉(zhuǎn)化為常量表達(dá)式,類似測試表達(dá)式實現(xiàn)重載的選擇(但需要添加一個冗余的 函數(shù)參數(shù)/函數(shù)返回值/模板參數(shù));

          • std::void_t 直接 檢查依賴 的成員/函數(shù)是否存在,不存在則無法重載(可以用于構(gòu)造謂詞,再通過 std::enable_if 判斷條件)。


          是否為特定的類型 的判斷,類似于代碼,將 unsigned Val 改為 typename Type;并把傳入的模板參數(shù)由 值參數(shù) 改為 類型參數(shù),根據(jù)最優(yōu)原則匹配重載。

          是否滿足某些條件 的判斷,在代碼中,展示了如何將 C 語言的基本類型數(shù)據(jù),轉(zhuǎn)換為 std::string 的函數(shù) ToString。代碼具體分為三個部分:

          • 首先定義三個 變量模板 :

            isNum/isStr/isBad,分別對應(yīng)了三個類型條件的謂詞(使用了 中的 std::is_arithmetic 和 std::is_same);

          • 然后根據(jù) SFINAE 規(guī)則:

            使用 std::enable_if 重載函數(shù) ToString,分別對應(yīng)了數(shù)值、C 風(fēng)格字符串和非法類型;

          • 在前兩個重載中:

            分別調(diào)用 std::to_string 和 std::string 構(gòu)造函數(shù);在最后一個重載中,通過 類型依賴 (type-dependent) 的 false 表達(dá)式(例如 sizeof (T) == 0)靜態(tài)斷言直接報錯(根據(jù) 兩階段名稱查找 (two-phase name lookup)的規(guī)定,如果直接使用 static_assert (false) 斷言,會在模板還沒實例化的第一階段無法通過編譯)。

          template <typename T>
          constexpr bool isNum = std::is_arithmetic<T>::value;

          template <typename T>
          constexpr bool isStr = std::is_same<T, const char *>::value;

          template <typename T>
          constexpr bool isBad = !isNum<T> && !isStr<T>;

          template <typename T>
          std::enable_if_t<isNum<T>, std::string> ToString (T num) {
              return std::to_string (num);
          }

          template <typename T>
          std::enable_if_t<isStr<T>, std::string> ToString (T str) {
              return std::string (str);
          }

          template <typename T>
          std::enable_if_t<isBad<T>, std::string> ToString (T bad) {
              static_assert (sizeof (T) == 0"neither Num nor Str");
          }

          auto a = ToString (1);  // std::to_string (num);
          auto b = ToString (1.0);  // std::to_string (num);
          auto c = ToString ("0x0");  // std::string (str);
          auto d = ToString (std::string {});  // not compile :-(

          代碼編譯時測試類型

          2.1.3 使用 if 進行編譯時測試

          對于初次接觸元編程的人,往往會使用 if 語句進行編譯時測試。代碼是代碼 一個 錯誤的寫法,很代表性的體現(xiàn)了元編程和普通編程的不同之處。

          template <typename T>
          std::string ToString (T val) {
              if (isNum<T>) return std::to_string (val);
              else if (isStr<T>) return std::string (val);
              else static_assert (!isBad<T>, "neither Num nor Str");
          }

          代碼編譯時測試類型的錯誤用法

          代碼中的錯誤在于:編譯代碼的函數(shù) ToString 時,對于給定的類型 T,需要進行兩次函數(shù)綁定 —— val 作為參數(shù)分別調(diào)用 std::to_string (val) 和 std::string (val),再進行一次靜態(tài)斷言 —— 判斷 !isBad<T> 是否為 true。這會導(dǎo)致:兩次綁定中,有一次會失敗。假設(shè)調(diào)用 ToString ("str"),在編譯這段代碼時,std::string (const char *) 可以正確的重載,但是 std::to_string (const char *) 并不能找到正確的重載,導(dǎo)致編譯失敗。

          假設(shè)是腳本語言,這段代碼是沒有問題的:因為腳本語言沒有編譯的概念,所有函數(shù)的綁定都在 運行時 完成;而靜態(tài)語言的函數(shù)綁定是在 編譯時 完成的。為了使得代碼的風(fēng)格用于元編程,C++ 17 引入了 constexpr-if—— 只需要把以上代碼中的 if 改為 if constexpr 就可以編譯了。

          constexpr-if 的引入讓模板測試更加直觀,提高了模板代碼的可讀性。代碼展示了如何使用 constexpr-if 解決編譯時選擇的問題;而且最后的 兜底 (catch-all) 語句,可以使用類型依賴的 false 表達(dá)式進行靜態(tài)斷言,不再需要 isBad<T> 謂詞模板(也不能直接使用 static_assert (false) 斷言)。

          template <typename T>
          std::string ToString (T val) {
              if constexpr (isNum<T>) return std::to_string (val);
              else if constexpr (isStr<T>) return std::string (val);
              else static_assert (sizeof (T) == 0"neither Num nor Str");
          }

          代碼編譯時測試類型的正確用法

          然而,constexpr-if 背后的思路早在 Visual Studio 2012 已出現(xiàn)了。其引入了 __if_exists 語句,用于編譯時測試標(biāo)識符是否存在。

          2.2 編譯時迭代

          編譯時迭代 和面向過程編程中的 循環(huán)語句 (loop statement) 類似:

          用于實現(xiàn)與 forwhiledo類似的循環(huán)邏輯。

          在 C++ 17 之前,和普通的編程不同,元編程的演算規(guī)則是純函數(shù)的,不能通過 變量迭代 實現(xiàn)編譯時迭代,只能用 遞歸 (recursion) 和 特化 的組合實現(xiàn)。一般思路是:提供兩類重載 —— 一類接受 任意參數(shù),內(nèi)部 遞歸 調(diào)用自己;另一類是前者的 模板特化 或 函數(shù)重載,直接返回結(jié)果,相當(dāng)于 遞歸終止條件。它們的重載條件可以是 表達(dá)式 或 類型。

          而 C++ 17 提出了 折疊表達(dá)式 (fold expression) 的語法,化簡了迭代的寫法。

          2.2.1 定長模板的迭代

          代碼展示了如何使用 編譯時迭代 實現(xiàn)編譯時計算階乘()。函數(shù) _Factor 有兩個重載:一個是對任意非負(fù)整數(shù)的,一個是對 0 為參數(shù)的。前者利用遞歸產(chǎn)生結(jié)果,后者直接返回結(jié)果。當(dāng)調(diào)用 _Factor<2> 時,編譯器會展開為 2 * _Factor<1>,然后 _Factor<1> 再展開為 1 * _Factor<0>,最后 _Factor<0> 直接匹配到參數(shù)為 0 的重載。

          template <unsigned N>
          constexpr unsigned _Factor () { return N * _Factor<N - 1> (); }

          template <>
          constexpr unsigned _Factor<0> () { return 1; }

          template <unsigned N>
          constexpr unsigned Factor = _Factor<N> ();

          static_assert (Factor<0> == 1"compile error");
          static_assert (Factor<1> == 1"compile error");
          static_assert (Factor<4> == 24"compile error");

          代碼編譯時迭代計算階乘(N!)


          2.2.2 變長模板的迭代

          為了遍歷變長模板的每個參數(shù),可以使用 編譯時迭代 實現(xiàn)循環(huán)遍歷。代碼實現(xiàn)了對所有參數(shù)求和的功能。函數(shù) Sum 有兩個重載:一個是對沒有函數(shù)參數(shù)的情況,一個是對函數(shù)參數(shù)個數(shù)至少為 1 的情況。和定長模板的迭代類似,這里也是通過 遞歸 調(diào)用實現(xiàn)參數(shù)遍歷。

          template <typename T>
          constexpr auto Sum () {
              return T (0);
          }

          template <typename T, typename... Ts>
          constexpr auto Sum (T arg, Ts... args) {
              return arg + Sum<T> (args...);
          }

          static_assert (Sum () == 0"compile error");
          static_assert (Sum (12.03) == 6"compile error");

          代碼編譯時迭代計算和(

          2.2.3 使用折疊表達(dá)式化簡編譯時迭代

          在 C++ 11 引入變長模板時,就支持了在模板內(nèi)直接展開參數(shù)包的語法;但該語法僅支持對參數(shù)包里的每個參數(shù)進行 一元操作 (unary operation);為了實現(xiàn)參數(shù)間的 二元操作 (binary operation),必須借助額外的模板實現(xiàn)(例如,代碼 定義了兩個 Sum 函數(shù)模板,其中一個展開參數(shù)包進行遞歸調(diào)用)。

          而 C++ 17 引入了折疊表達(dá)式,允許直接遍歷參數(shù)包里的各個參數(shù),對其應(yīng)用 二元運算符 (binary operator) 進行 左折疊 (left fold) 或 右折疊 (right fold)。代碼使用初始值為 0 的左折疊表達(dá)式,對代碼進行改進。

          template <typename... Ts>
          constexpr auto Sum (Ts... args) {
              return (0 + ... + args);
          }

          static_assert (Sum () == 0"compile error");
          static_assert (Sum (12.03) == 6"compile error");

          代碼編譯時折疊表達(dá)式計算和(


          3. 元編程的基本應(yīng)用

          利用元編程,可以很方便的設(shè)計出 類型安全 (type safe)運行時高效 (runtime effective) 的程序。到現(xiàn)在,元編程已被廣泛的應(yīng)用于 C++ 的編程實踐中。例如,Todd Veldhuizen 提出了使用元編程的方法構(gòu)造 表達(dá)式模板 (expression template),使用表達(dá)式優(yōu)化的方法,提升向量計算的運行速度;K. Czarnecki 和 U. Eisenecker 利用模板實現(xiàn) Lisp 解釋器。

          盡管元編程的應(yīng)用場景各不相同,但都是三類基本應(yīng)用的組合:數(shù)值計算 (numeric computation)類型推導(dǎo) (type deduction) 和 代碼生成 (code generation)。例如,在 BOT Man 設(shè)計的 對象關(guān)系映射 (object-relation mapping, ORM) 中,主要使用了 類型推導(dǎo) 和 代碼生成 的功能。根據(jù) 對象 (object) 在 C++ 中的類型,推導(dǎo)出對應(yīng)數(shù)據(jù)庫 關(guān)系 (relation) 中元組各個字段的類型;將對 C++ 對象的操作,映射到對應(yīng)的數(shù)據(jù)庫語句上,并生成相應(yīng)的代碼。

          3.1 數(shù)值計算

          作為元編程的最早的應(yīng)用,數(shù)值計算可以用于 編譯時常數(shù)計算 和 優(yōu)化運行時表達(dá)式計算

          編譯時常數(shù)計算 能讓程序員使用程序設(shè)計語言,寫編譯時確定的常量;而不是直接寫常數(shù)(迷之?dāng)?shù)字 (magic number))或 在運行時計算這些常數(shù)。例如,幾個例子都是編譯時對常數(shù)的計算。

          最早的有關(guān)元編程 優(yōu)化表達(dá)式計算 的思路是 Todd Veldhuizen 提出的。利用表達(dá)式模板,可以實現(xiàn)部分求值、惰性求值、表達(dá)式化簡等特性。

          3.2 類型推導(dǎo)

          除了基本的數(shù)值計算之外,還可以利用元編程進行任意類型之間的相互推導(dǎo)。例如,在 領(lǐng)域特定語言 (domain-specific language) 和 C++ 語言原生結(jié)合時,類型推導(dǎo)可以實現(xiàn)將這些語言中的類型,轉(zhuǎn)化為 C++ 的類型,并保證類型安全。

          BOT Man 提出了一種能編譯時進行 SQL 語言元組類型推導(dǎo)的方法。C++ 所有的數(shù)據(jù)類型都不能為 NULL;而 SQL 的字段是允許為 NULL 的,所以在 C++ 中使用 std::optional 容器存儲可以為空的字段。通過 SQL 的 outer-join 拼接得到的元組的所有字段都可以為 NULL,所以 ORM 需要一種方法:把字段可能是 std::optional<T> 或 T 的元組,轉(zhuǎn)化為全部字段都是 std::optional<T> 的新元組。

          template <typename T> struct TypeToNullable {
              using type = std::optional<T>;
          };
          template <typename T> struct TypeToNullable <std::optional<T>> {
              using type = std::optional<T>;
          };

          template <typename... Args>
          auto TupleToNullable (const std::tuple<Args...> &) {
              return std::tuple<typename TypeToNullable<Args>::type...> {};
          }

          auto t1 = std::make_tuple (std::optional<int> {}, int {});
          auto t2 = TupleToNullable (t1);
          static_assert (!std::is_same<
                         std::tuple_element_t<0decltype (t1)>,
                         std::tuple_element_t<1decltype (t1)>
          >::value, "compile error");
          static_assert (std::is_same<
                         std::tuple_element_t<0decltype (t2)>,
                         std::tuple_element_t<1decltype (t2)>
          >::value, "compile error");

          代碼類型推導(dǎo)

          代碼展示了這個功能:

          • 定義TypeToNullable并對 std::optional 進行特化,作用是將 std::optional 和 T 自動轉(zhuǎn)換為 std::optional

          • 定義 TupleToNullable,拆解元組中的所有類型,轉(zhuǎn)化為參數(shù)包,再把參數(shù)包中所有類型分別傳入 TypeToNullable,最后得到的結(jié)果重新組裝為新的元組。


          3.3 代碼生成

          和泛型編程一樣,元編程也常常被用于代碼的生成。但是和簡單的泛型編程不同,元編程生成的代碼往往是通過 編譯時測試 和 編譯時迭代 的演算推導(dǎo)出來的。例如,代碼就是一個將 C 語言基本類型轉(zhuǎn)化為 std::string 的代碼的生成代碼。

          在實際項目中,我們往往需要將 C++ 數(shù)據(jù)結(jié)構(gòu),和實際業(yè)務(wù)邏輯相關(guān)的 領(lǐng)域模型 (domain model) 相互轉(zhuǎn)化。例如,將承載著領(lǐng)域模型的 JSON 字符串 反序列化 (deserialize) 為 C++ 對象,再做進一步的業(yè)務(wù)邏輯處理,然后將處理后的 C++ 對象 序列化 (serialize) 變?yōu)?JSON 字符串。而這些序列化/反序列化的代碼,一般不需要手動編寫,可以自動生成。

          BOT Man 提出了一種基于 編譯時多態(tài) (compile-time polymorphism) 的方法,定義領(lǐng)域模型的 模式 (schema),自動生成領(lǐng)域模型和 C++ 對象的序列化/反序列化的代碼。這樣,業(yè)務(wù)邏輯的處理者可以更專注于如何處理業(yè)務(wù)邏輯,而不需要關(guān)注如何做底層的數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換。


          4. 元編程的主要難點

          由于 C++ 語言設(shè)計層面上沒有專門考慮元編程的相關(guān)問題,所以實際元編程難度較大。元編程的難點主要有四類:復(fù)雜性、實例化錯誤、代碼膨脹、調(diào)試模板。

          4.1 復(fù)雜性

          由于元編程的語言層面上的限制較大,所以許多的元編程代碼使用了很多的 編譯時測試 和 編譯時迭代 技巧,可讀性 (readability) 都比較差。另外,由于巧妙的設(shè)計出編譯時能完成的演算也是很困難的,相較于一般的 C++ 程序,元編程的 可寫性 (writability) 也不是很好。

          現(xiàn)代 C++ 也不斷地增加語言的特性,致力于降低元編程的復(fù)雜性:

          • C++ 11 的 別名模板提供了對模板中的類型的簡記方法;

          • C++ 14 的 變量模板提供了對模板中常量的簡記方法;

          • C++ 17 的 `constexpr-if`提供了 編譯時測試 的新寫法;

          • C++ 17 的 折疊表達(dá)式降低了 編譯時迭代 的編寫難度。


          基于 C++ 14 的 泛型 lambda 表達(dá)式,元編程庫 Boost.Hana 提出了 不用模板就能元編程 的理念,宣告從 模板元編程 (template metaprogramming) 時代進入 現(xiàn)代元編程 (modern metaprogramming) 時代。其核心思想是:只需要使用 C++ 14 的泛型 lambda 表達(dá)式和 C++ 11 的 constexpr/decltype,就可以快速實現(xiàn)元編程的基本演算了。

          4.2 實例化錯誤

          模板的實例化 和 函數(shù)的綁定 不同:在編譯前,前者對傳入的參數(shù)是什么,沒有太多的限制;而后者則根據(jù)函數(shù)的聲明,確定了應(yīng)該傳入?yún)?shù)的類型。而對于模板實參內(nèi)容的檢查,則是在實例化的過程中完成的。所以,程序的設(shè)計者在編譯前,很難發(fā)現(xiàn)實例化時可能產(chǎn)生的錯誤。

          為了減少可能產(chǎn)生的錯誤,Bjarne Stroustrup 等人提出了在 語言層面 上,給模板上引入 概念 (concept)。利用概念,可以對傳入的參數(shù)加上 限制 (constraint),即只有滿足特定限制的類型才能作為參數(shù)傳入模板。例如,模板 std::max 限制接受支持運算符 < 的類型傳入。但是由于各種原因,這個語言特性一直沒有能正式加入 C++ 標(biāo)準(zhǔn)(可能在 C++ 20 中加入)。盡管如此,編譯時仍可以通過 編譯時測試 和 靜態(tài)斷言 等方法實現(xiàn)檢查。

          另外,編譯時模板的實例化出錯位置,在調(diào)用層數(shù)較深處時,編譯器會提示每一層實例化的狀態(tài),這使得報錯信息包含了很多的無用信息,很難讓人較快的發(fā)現(xiàn)問題所在。BOT Man 提出了一種 短路編譯 (short-circuit compiling) 的方法,能讓基于元編程的  (library),給用戶提供更人性化的編譯時報錯。具體方法是,在 實現(xiàn) (implementation) 調(diào)用需要的操作之前,接口 (interface) 先檢查是傳入的參數(shù)否有對應(yīng)的操作;如果沒有,就通過短路的方法,轉(zhuǎn)到一個用于報錯的接口,然后停止編譯并使用 靜態(tài)斷言 提供報錯信息。

          4.3 代碼膨脹

          由于模板會對所有不同模板實參都進行一次實例化,所以當(dāng)參數(shù)的組合很多的時候,很可能會發(fā)生 代碼膨脹 (code bloat),即產(chǎn)生體積巨大的代碼。這些代碼可以分為兩種:死代碼 (dead code) 和 有效代碼 (effective code)

          在元編程中,很多時候只關(guān)心推導(dǎo)的結(jié)果,而不是過程。例如,代碼中只關(guān)心最后的 Factor<4> == 24,而不需要中間過程中產(chǎn)生的臨時模板。但是在 N 很大的時候,編譯會產(chǎn)生很多臨時模板。這些臨時模板是 死代碼,即不被執(zhí)行的代碼。所以,編譯器會自動優(yōu)化最終的代碼生成,在 鏈接時 (link-time) 移除這些無用代碼,使得最終的目標(biāo)代碼不會包含它們。

          另一種情況下,展開的代碼都是 有效代碼,即都是被執(zhí)行的,但是又由于需要的參數(shù)的類型繁多,最后的代碼體積仍然很大。編譯器很難優(yōu)化這些代碼,所以程序員應(yīng)該在 設(shè)計時編碼代碼膨脹。Bjarne Stroustrup 提出了一種消除 冗余運算 (redundant calculation) 的方法,用于縮小模板實例體積。具體思路是,將不同參數(shù)實例化得到的模板的 相同部分 抽象為一個 基類 (base class),然后 “繼承” 并 “重載” 每種參數(shù)情況的 不同部分,從而實現(xiàn)更多代碼的共享。

          例如,在 std::vector 的實現(xiàn)中,對 T * 和 void * 進行了特化;然后將所有的 T * 的實現(xiàn) 繼承 到 void * 的實現(xiàn)上,并在公開的函數(shù)里通過強制類型轉(zhuǎn)換,進行 void * 和 T * 的相互轉(zhuǎn)換;最后這使得所有的指針的 std::vector 就可以共享同一份實現(xiàn),從而避免了代碼膨脹。

          template <typename T> class vector;       // general
          template <typename T> class vector<T *>;  // partial spec
          template <> class vector<void *>;         // complete spec

          template <typename T>
          class vector<T *> : private vector<void *>
          {
              using Base = Vector<void?>;
          public:
              T?& operator[] (int i) {
                  return reinterpret_cast<T?&>(Base::operator[] (i));
              }
              ...
          }

          代碼特化 std::vector 避免代碼膨脹


          4.4 調(diào)試模板

          元編程在運行時主要的難點在于:對模板代碼的 調(diào)試 (debugging)。如果需要調(diào)試的是一段通過很多次的 編譯時測試和 編譯時迭代展開的代碼,即這段代碼是各個模板的拼接生成的(而且展開的層數(shù)很多);那么,調(diào)試時需要不斷地在各個模板的 實例 (instance) 間來回切換。這種情景下,調(diào)試人員很難把具體的問題定位到展開后的代碼上。

          所以,一些大型項目很少使用復(fù)雜的代碼生成技巧,而是通過傳統(tǒng)的代碼生成器生成重復(fù)的代碼,易于調(diào)試。例如 Chromium 的 通用擴展接口 (common extension api) 通過定義 JSON/IDL 文件,通過代碼生成器生成相關(guān)的 C++ 代碼。


          5. 總結(jié)

          C++ 元編程的出現(xiàn),是一個無心插柳的偶然 —— 人們發(fā)現(xiàn) C++ 語言提供的模板抽象機制,能很好的被應(yīng)用于元編程上。借助元編程,可以寫出 類型安全運行時高效 的代碼。但是,過度的使用元編程,一方面會 增加編譯時間,另一方面會 降低程序的可讀性。不過,在 C++ 不斷地演化中,新的語言特性被不斷提出,為元編程提供更多的可能。

          往期推薦




          ? 專輯 | 趣味設(shè)計模式
          專輯 | 音視頻開發(fā)
          專輯 | C++ 進階
          專輯 | 超硬核 Qt
          專輯 | 玩轉(zhuǎn) Linux
          專輯 | GitHub 開源推薦
          ? 專輯 | 程序人生


          關(guān)注公眾「高效程序員」??一起優(yōu)秀!

          回復(fù)“1024”,送你一份程序員大禮包。
          瀏覽 64
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩三级片网站在线观看 | 荫蒂高潮大荫蒂毛茸茸主播 | 91天天干天天日 | 欧美精品在线播放 | 日韩欧美国产黄色电影 |