1. 程式人生 > >C++ 模板元程式(二)

C++ 模板元程式(二)

第一章

你可以通過此章進行一下熱身。由此也可以測試一下你所使用的工具,瞭解一些基本概念及術語。在本章結束時,你最起碼已經有了本書所涉及內容的一個粗略概念,並且你要(我們也希望)繼續往下學習。

1.1 讓我們開始吧

模板元程式的一個好處就是與以前舊的良好傳統系統共享一種特點,就是一性生就之後,只要其能如預期工作,就無需瞭解其底下是如何實現的。

為了讓你確信這一點,可以來一小段用 C++ 模板寫成的元程式:

#include "libs/mpl/book/chapter1/binary.hpp"
#include <iostream>

int main()
{
std::cout << binary<101010>::value << std::endl;
return 0;
}

就算你對於二進位制的運算了如指掌,但是如果不實際執行上面的程式碼,你是無法知道其輸出是什麼的,因此我們依然不無叨擾地建議你在你的編譯器上編譯一下上面的小程式,一來可以增強你的自信心,二來呢也可以測試一下你所用的編譯器處理本書中的程式碼的能力。這面的程式意思是輸出二進位制數值所對應的十進位制數 101010:

42

到標準輸出上。

1.2 那麼,什麼是元程式呢?

如果你將元程式(Metaprogramming)分拆開來看,在字面上的意思就是“程式的程式”[1],一個少了一點詩意的解釋就是:元程式就是操作程式碼的程式。初聽起來很怪的概念,但是其實你可以已經熟悉了其中的幾個這樣的巨獸了。你的 C++ 編譯器就是一個很好的例子:它操作你的 C++ 程式碼,以產生彙編程式碼或者機器程式碼。

[1] 在哲學上,如上面的, 在 Programing 之字首以 Meta 以來表示“關於”,或者“更高一級的描述”,這是從希臘語言“Beyond”或者“Behind”引申而來的。

還有由 YACC 生成的語法分析器,是另一個例子。YACC 的輸入是根據一種語法規則的寫成的並且將其行為也附加其上的高級別語法分析器描述語言。如用前向優先規則分析並求值某個算術表示式,我們可以將下面的程式碼輸入給 YACC:

   expression : term
              | expression '+' term { $$ = $1 + $3; }
              | expression '-' term { $$ = $1 - $3; };
   term : factor
        | term '*' factor { $$ = $1 * $3; }
        | term '/' factor { $$ = $1 / $3; };

   factor : INTEGER
          | group;

   group : '(' expression ')';

相應地,YACC 將產生 C/C++ 原始碼(夾在其他部分之中):一個是 yyparse 函式,此函式可以根據規則來對語法進行分析並執行相應的動作:[2]

[2]  這也表明,我們也實現了 yylex 函式來將文字進行符號化,第十章有更詳細的例子。當然如果你夠狠,可以開啟 YACC 的手冊。

   int main()
   {
     extern int yyparse();
     return yyparse();
   }

YACC 的操作大多在於語法分析區領域的設計,因此我們也將 YACC  的輸入語言叫做這個系統的“領域語言”。由於使用者的程式一般都需要一個通用的程式設計系統,並且必須與所生成的語法分析器進行互動。YACC 將“領域語言”轉換成主語言,也就是 C,之後使用者又將其與自己的其他程式碼進行連線。因此“領域語言”經過了兩個過程的轉換,而且使用者也非常瞭解“領域語言”與其程式碼之間的邊界。

1.3 主語言中的元程式

YACC 所轉換的“領域語言”與其主語言,兩者是不同的。更為有趣的元程式的形式出現於像 Scheme 這樣的語言中。在 Scheme 語言中的元程式作用會定義一個語言子集,此子集是合法的 Scheme 程式,而元程式經過同一個轉換步驟來處理使用者的程式。程式設計師在二進位制程式、元程式以及寫“領域語言”之間進行來回穿梭,但卻沒有意識這種變換,他們可以將多個領域無縫地結合在一個程式設計系統中。

令人興奮的是,如果你有一個 C++ 編譯器,那麼在你的指尖也有這樣的能力,書的後面章節就是向你展示何時、何地以及如何來釋放這樣的威力。

1.4 C++ 中的元程式

在 C++ 中發現模板機制可以為我們提供功能豐富的本地語言的元程式純屬偶然。在這一節中,我們就展示在 C++ 元程式中所使用的一些基本技巧及手法。

1.4.1 數值計算

最早的 C++ 元程式就是在編譯期進行資料計算。最早一個提交給 C++ 委員會的元程式則是由 Erwin Unruh 提供的。這實際是一個無法通過編譯的程式碼段,而這些程式碼編譯時出錯的資訊中輸出了一系列的質數!

由於非法程式碼(無法通過編譯)是無法在一個大型系統中有效應用的,那麼我們再測試一下實際的應用。下面的元程式(這是我們這前出現的編譯器測試程式碼中的核心部分)將無符號的整數轉當成二進位制表示,使得我們可以用我們可識別的二進位制方式來表達某個十進位制數:

   template <unsigned long N>
   struct binary
   {
       static unsigned const value
          = binary<N/10>::value << 1   // prepend higher bits
            | N%10;                    // to lowest bit
   };

   template <>                           // specialization
   struct binary<0>                      // terminates recursion
   {
       static unsigned const value = 0;
   };

   unsigned const one   =    binary<1>::value;
   unsigned const three =   binary<11>::value;
   unsigned const five  =  binary<101>::value;
   unsigned const seven =  binary<111>::value;
   unsigned const nine  = binary<1001>::value;

如果你還在疑惑:“程式在哪?”,我們提請你注意我們在訪問型別 binary<N> 的內嵌成員 ::value 的地方。binary 模板會用更小的 N 來例項化,直到 N 變為零,而這將通過一個特化版本來進行條件的終結。這樣的過程相當有遞迴函式的呼叫過程的味道。但是元程式還是一個函式?最終編譯器就被我們用來解析了我們的元程式。

錯誤檢測:

在上面的程式碼中,沒有什麼能夠限制我們向 binary 輸送一個 678 這樣值,而這樣的十進位制數並不是一個有效的二進位制表示,結果將是一個非常奇怪的結果(將會是:6x22 + 7x21 + 8x20)。但是無論怎樣,678 這樣的一個數表示了一種使用者邏輯上的Bug,在第三章中,我們將向你層示如何保證使得十進位制的表示中只含有 0 和 1。

由於 C++ 中揭示了在執行時以及編譯時兩種不同運算的明顯區別,元程式就是執行時的對面,即編譯時。如 Scheme 中,C++ 使用相同的語言像完成普通程式一樣來完成元程式的編寫,但是在 C++ 中只有編譯時的語言子集才能用在元程式中。用下面的程式碼與上面的程式碼進行比較:

   unsigned binary(unsigned long N)
   {
       return N == 0 ? 0 : N%10 + 2 * binary(N/10);
   }

執行時與編譯時關鍵不同在於結束條件的處理方式上。在元程式中當 N 為令時結束條件是通過模板特化來完成的。而這幾乎是所有 C++ 的元程式所使用方式,儘管有時它隱在元程式庫的後面。

而另一個不同在於,如下面這個例子中所提示的那樣,我們將遞迴換成了一個 for 迴圈:

   unsigned binary(unsigned long N)
   {
       unsigned result = 0;
       for (unsigned bit = 0x1; N; N /= 10, bit <<= 1)
       {
           if (N%10)
               result += bit;
       }
       return result;
   }

儘管看起來比遞迴版本的程式碼要多一些,但是多數的 C++ 程式設計師還是喜歡這一個版本的程式碼。至少是因為執行時迭代要比遞迴更有效率。

而編譯時的 C++ 版本由於與 Haskell 一樣具有一個特徵我們將其稱為“純函式式語言”,這就是:(元)資料是不可變的,(元)函式也沒副作用。這導致C++編譯時並沒有與執行時函式中使用的 non-const 相對應的任何元素。因為你不能寫一個在結束時不判斷可變狀態的迴圈(無限迴圈),迭代而很快就會超過編譯時處理能力之外。因此,遞迴是 C++ 元程式的慣用法。

1.4.2 型別計算

比 C++ 能處理編譯時期計算問題更強的能力是其可以在編譯時期計算型別。而且實際上,型別的計算則正是本書所涉及的內容。在我們的下一章最前部分,我們將討論一些例子。當我們看過些例子,你將會可能將模板超程式設計認為就是型別計算。

儘管你可能已經讀了第二章的內容並理解了型別計算,我們也還會向你傳達這種能力。回憶一下 YACC 中的資料計算表示式?C++ 元程式最終使我們不需要使用某個轉換器去取得某種能力或者方便性。從 Boost 的Spirit 庫中使用恰當的程式碼,下面的 C++ 程式碼就可以完成 YACC 資料計算表示式的功能:

  expr =
         ( term[expr.val = _1] >> '+' >> expr[expr.val += _1] )
       | ( term[expr.val = _1] >> '-' >> expr[expr.val -= _1] )
       | term[expr.val = _1]
       ;

   term =
         ( factor[term.val = _1] >> '*' >> term[term.val *= _1] )
       | ( factor[term.val = _1] >> '/' >> term[term.val /= _1] )
       | factor[term.val = _1];

   factor =
         integer[factor.val = _1]
       | ( '(' >> expr[factor.val = _1] >> ')' ) ;

每一個表示式都產生一個計算其右手邊求值的分析及計算過程的物件,對賦給左邊的物件。當被呼叫時,每一個物件的行為,都由生成這個物件的型別來決定的。而每個型別的表示式的計算則是由一系列的元程式與相關的操作符來完成的。

就如同 YACC,Spirit 元程式庫是從語法規範來產生語法分析器的元程式。但是又與 YACC 不同的是,Spirit 所定義的語言是C++語言的一個子集。如果此時你還不能看明白這一切是如何發生的,不用擔心,讀完這本書後,你就會明白了。



相關推薦

C++ 模板程式

第一章 你可以通過此章進行一下熱身。由此也可以測試一下你所使用的工具,瞭解一些基本概念及術語。在本章結束時,你最起碼已經有了本書所涉及內容的一個粗略概念,並且你要(我們也希望)繼續往下學習。 1.1 讓我們開始吧 模板元程式的一個好處就是與以前舊的良好傳統系統共享一種特點,就

C++ 模板詳解

創建 規則 error ++ 例如 public err iostream () 四、類模板的默認模板類型形參   1、可以為類模板的類型形參提供默認值,但不能為函數模板的類型形參提供默認值。函數模板和類模板都可以為模板的非類型形參提供默認值。   2、類模板的類型形

C++模板詳解

轉載自:http://www.cnblogs.com/gw811/archive/2012/10/25/2736224.html C++模板 四、類模板的預設模板型別形參   1、可以為類模板的型別形參提供預設值,但不能為函式模板的型別形參提供預設值。函式模板

模板程式十一

第三章 深入元函式 到此所做的鋪墊,我們已經準備好了探索模板元程式技術最基本的使用例子:給傳統的未檢查的操作新增靜態型別檢查。我們從科學與工程中的實際例子出發一看究竟,這個例子幾乎在所有的數值程式碼中都有應用。一路走來,你會學到一些最重要的新概念,並對在更高級別上使用

C++面向物件總結--友函式

類的友元函式 類的友元函式是定義在類外部,但有權訪問類的私有成員(private)和保護成員(protected)成員。儘管友元函式的原型在類的定義中出現過,但是友元函式並不是成員函式。#includ

一天練習一個小C/C++程式 指標到底該如何使用

以前寫C基礎的時候,寫過很多次指標的例子,那到底指標在C專案中會怎麼應用呢? 通常有兩方面: 一:函式的傳參:我們已經說過,函式傳參除非傳的是記憶體地址,要不修改後的值對傳入之前的值沒有影響。 二:跨函式修改值: 其實我覺得這個和第一條一樣。。都是為了使其在另一個函式的修改

c++11-17 模板核心知識—— 類模板

- [類模板宣告、實現與使用](#類模板宣告實現與使用) - [Class Instantiation](#class-instantiation) - [使用類模板的部分成員函式](#使用類模板的部分成員函式) - [Concept](#concept) - [友元](#友元) - [方式一](#方

C語言天天練】statickeyword

修飾 weight () main函數 class code keyword spa tail 引言: statickeyword不僅能夠修飾變量。並且能夠修飾函數。了解它的使用方法,不僅對閱讀別人的代碼有幫助,也有助於自己寫出更加健壯的

C++構造函數

frame 筆記 自動轉換 數據類型 public clas 並不是 調用 這樣的 本篇是介紹C++的構造函數的第二篇(共二篇),屬於讀書筆記,對C++進行一個系統的復習。 復制構造函數 復制構造函數是構造函數的一種,也被稱為拷貝構造函數,他只有一個參數,參數類型是本類的引

Python和C|C++的混編:利用Cython進行混編

cde uil 有時 當前 class def 將在 python 混編 還能夠使用Cython來實現混編 1 下載Cython。用python setup.py install進行安裝 2 一個實例 ① 創建helloworld文件夾創建hellowor

讀書筆記--C陷阱與缺陷

ase 結果 erro bit 使用 功能 錯誤 多層 gnu 第二章 1. 理解函數聲明 書中分析了復雜的類型聲明方式,也說明了使用typedef聲明會更好理解,推薦大家使用typedef進行函數聲明。 書中類型分析一層一層挖掘,讓讀者可以理解多層嵌套的類型含義,有

我的C#跨平臺之旅:開發一組標準的Restful API

ref 運行 mar margin bruce ora soft left 啟用 添加NuGet引用:Microsoft.AspNet.WebApi.Owin 在啟動類啟用WebApi; 添加一個Controller類,代碼如下: 運行程序

C#常見問題總結

7月 組件 圖片 水晶報表 datetime 控制臺 col orm value 1、erp系統可以在具有固定ip的擁有多層服務器的局域網中使用嗎?如何使用解決方法:把ini.配置文件字符串中的服務器名改成服務器的,把debug文件夾拷到其他機器上就行,服務器上的服務器名是

C++ Primer 筆記】 變量

class tro ++ div bsp mail post c++ 系列 本系列文章由 Nick-Pem 原創編寫,轉載請說明出處。 作者:Nick-Pem  郵箱:[email protected] 留坑【C++ Primer 筆記

C#上位機開發

styles 寫代碼 面向 ext size ring 入口 查詢法 命令   上一篇大致了解了一下單片機實際項目開發中上位機開發部分的內容已經VS下載與安裝,按照編程慣例,接下來就是“Hello,World!” 1、新建C#項目工程    首先選擇新建Windows窗體應

c#多線程——同步和異步

沒有 之前 什麽 adk 返回 con bsp cti csu 1、什麽是異步   如果一個程序調用某個方法,等待其執行所有處理後才繼續執行,我們稱這樣的方法是同步的。   如果一個程序調用某個方法,在該方法處理完成之前就返回到調用方法,則這個方法是異步的。 異步的好處在於

WPF Button按鈕模板樣式修改

本章講述:Button模板樣式修改,不顯示邊框,只顯示圖示,點選按鈕圖示動態旋轉; 主要採用採用圖片做背景和資料觸發器,觸發故事板(Storyboard)事件,實現按鈕中圖片按角度旋轉; <Button Width="30" Height="30"> <Butt

C語言基礎篇運算子

導航:   2.1 算數運算子   2.2 邏輯運算子   2.3 位運算   2.4 賦值運算   2.5 記憶體訪問符號 ----->x<------------->x&

SQLite - C/C++接口 API

mage std per src argc sele pdb face module to 1、打開數據庫 SQLITE_API int sqlite3_open16(   const void *filename, /* Database filename (U

詳解C#委託和事件

  一、當我們使用關鍵字delegate宣告一個自定義委託型別時,實際上是聲明瞭一個該名稱的類型別,繼承自抽象類System.MulticastDelegate,還包含例項方法Invoke、BeginInvoke、EndInvoke:   public delegate void MyDelegate