編譯階段 屬性和靜態斷言

2021-10-06 17:14:00 字數 4927 閱讀 2514

編譯階段的目標是生成計算機可識別的機器碼(machine instruction code)。而編碼階段和預處理階段的主要工作還是「文字編輯」,生成的是人類可識別的原始碼(source code)。編譯階段的特殊性在於,它看到的都是 c++ 語法實體,比如 typedef、using、template、struct/class 這些關鍵字定義的型別,而不是執行階段的變數。所以,這時的程式設計思維方式與平常大不相同。

比如說,讓編譯器遞迴計算斐波那契數列,這已經算是乙個比較容易理解的編譯階段數值計算用法了:

template

<

int n>

struct fib // 遞迴計算斐波那契數列

;template

<

>

struct fib<

0>

// 模板特化計算fib<0>

;template

<

>

struct fib<

1>

// 模板特化計算fib<1>

;// 呼叫後輸出2,3,5,8

cout << fib<

2>

::value << endl;

cout << fib<

3>

::value << endl;

cout << fib<

4>

::value << endl;

cout << fib<

5>

::value << endl;

對於編譯器來說,可以在一瞬間得到結果,但你要搞清楚它的執行過程,就得在大腦裡把 c++ 模板特化的過程走一遍。整個過程無法除錯,完全要靠自己去推導,特別「累人」。(你也可以把編譯器想象成是一種特殊的「虛擬機器」,在上面跑的是只有編譯器才能識別、處理的**。)

在 c++11 之前,標準裡沒有規定這樣的東西,但 gcc、vc 等編譯器發現這樣做確實很有用,於是就實現出了自己「編譯指令」,在 gcc 裡是「__ attribute __」,在 vc 裡是「__declspec」。不過因為它們不是標準,所以名字顯得有點「怪異」。到了 c++11,標準委員會終於認識到了「編譯指令」的好處,於是就把「民間」用法公升級為「官方版本」,起了個正式的名字叫「屬性」。你可以把它理解為給變數、函式、類等「貼」上乙個編譯階段的「標籤」,方便編譯器識別處理。「屬性」沒有新增關鍵字,而是用兩對方括號的形式「[[…]]」,方括號的中間就是屬性標籤(看著是不是很像一張方方正正的便籤條)。所以,它的用法很簡單,比 gcc、vc 的都要簡潔很多。

[

[noreturn]

]// 屬性標籤

intfunc

(bool flag)

// 函式絕不會返回任何值

[

[deprecated

("deadline:2020-12-31")]

]// c++14 or later

intold_func()

;

於是,任何用到這個函式的程式都會在編譯時看到這個標籤,報出一條警告:

warning: 『int

old_func

()』 is deprecated: deadline:

2020-12

-31[-wdeprecated-declarations]

當然,程式還是能夠正常編譯的,但這種強制的警告形式會「提醒」使用者舊介面已經被廢棄了,應該盡快遷移到新介面。顯然,這種形式要比毫無約束力的文件或者注釋要好得多。目前的 c++17 和 c++20 又增加了五六個新屬性,比如 fallthrough、likely,但我覺得,標準委員會的態度還是太「保守」了,在實際的開發中,這些真的是不夠用。好在「屬性」也支援非標準擴充套件,允許以類似名字空間的方式使用編譯器自己的一些「非官方」屬性,比如,gcc 的屬性都在「gnu::」裡。下面我就列出幾個比較有用的(全部屬性可參考gcc 文件)。

deprecated:與 c++14 相同,但可以用在 c++11 裡。

unused:用於變數、型別、函式等,表示雖然暫時不用,但最好保留著,因為將來可能會用。

constructor:函式會在 main() 函式之前執行,效果有點像是全域性物件的建構函式。

destructor:函式會在 main() 函式結束之後執行,有點像是全域性物件的析構函式。

always_inline:要求編譯器強制內聯函式,作用比 inline 關鍵字更強。

hot:標記「熱點」函式,要求編譯器更積極地優化。

以上用法說明 。

以unused為例,在沒有這個屬性的時候,如果有暫時用不到的變數,我們只能用「(void) var;」的方式假裝用一下,來「騙」過編譯器,屬於「不得已而為之」的做法。那麼現在,我們就可以用「unused」屬性來清楚地告訴編譯器:這個變數我暫時不用,請不要過度緊張,不要發出警告來煩我:

[

[gnu::unused]

]// 宣告下面的變數暫不使用,不是錯誤

int nouse;

「屬性」像是給編譯器的乙個「提示」「告知」,無法進行計算,還算不上是程式設計,而接下來要講的「靜態斷言」,就有點編譯階段寫程式的味道了。

assert()用來斷言乙個表示式必定為真。比如說,數字必須是正數,指標必須非空、函式必須返回 true:

assert

(i >0&&

"i must be greater than zero");

assert

(p !=

nullptr);

assert

(!str.

empty()

);

當程式(也就是 cpu)執行到 assert 語句時,就會計算表示式的值,如果是 false,就會輸出錯誤訊息,然後呼叫 abort() 終止程式的執行。注意,assert 雖然是乙個巨集,但在預處理階段不生效,而是在執行階段才起作用,所以又叫「動態斷言」。

模擬一下 assert,它是編譯階段裡檢測各種條件的「斷言」,編譯器看到 static_assert 也會計算表示式的值,如果值是 false,就會報錯,導致編譯失敗。不過它是乙個專門的關鍵字,而不是巨集。因為它只在編譯時生效,執行階段看不見,所以是「靜態」的。

比如說,這節課剛開始時的斐波拉契數列計算函式,可以用靜態斷言來保證模板引數必須大於等於零:

template

<

int n>

struct fib

;

再比如說,要想保證我們的程式只在 64 位系統上執行,可以用靜態斷言在編譯階段檢查 long 的大小,必須是 8 個位元組(當然,你也可以換個思路用預處理程式設計來實現)。

static_assert

(sizeof

(long

)>=8,

"must run on x64");

static_assert

(sizeof

(int)==

4,"int must be 32bit"

);

注意,static_assert 執行在編譯階段,只能看到編譯時的常數和型別,看不到執行時的變數、指標、記憶體資料等,是「靜態」的,所以不要簡單地把 assert 的習慣搬過來用。比如,下面的**想檢查空指標,由於變數只能在執行階段出現,而在編譯階段不存在,所以靜態斷言無法處理。就像編譯器那樣去執行,看看斷言的表示式是不是能夠在編譯階段算出結果。

char

* p =

nullptr

;static_assert

(p ==

nullptr

,"some error.");

// 錯誤用法

不過這句話說起來容易做起來難,計算數字還好說,在泛型程式設計的時候,怎麼檢查模板型別呢?比如說,斷言是整數而不是浮點數、斷言是指標而不是引用、斷言型別可拷貝可移動……這些檢查條件表面上看好像是「不言自明」的,但要把它們用 c++ 語言給精確地表述出來,可就沒那麼簡單了。所以,想要更好地發揮靜態斷言的威力,還要配合標準庫里的「type_traits」,它提供了對應這些概念的各種編譯期「函式」。

// 假設t是乙個模板引數,即template

static_assert

( is_integral

::value,

"int");

static_assert

( is_pointer

::value,

"ptr");

static_assert

( is_default_constructible

::value,

"constructible"

);

「static_assert」裡的表示式樣子很奇怪,既有模板符號「<>」,又有作用域符號「::」,與執行階段的普通表示式大相徑庭,初次見到這樣的**一定會嚇一跳。這也是沒有辦法的事情。因為 c++ 本來不是為編譯階段程式設計所設計的。受語言的限制,編譯階段程式設計就只能「魔改」那些傳統的語法要素了:把類當成函式,把模板引數當成函式引數,把「::」當成 return 返回值。說起來,倒是和「函式式程式設計」很神似,只是它執行在編譯階段。由於「type_traits」已經初步涉及模板元程式設計的領域,不太好一下子解釋清楚,所以,在這裡就不再深入介紹了。

「屬性」相當於編譯階段的「標籤」,用來標記變數、函式或者類,讓編譯器發出或者不發出警告,還能夠手工指定**的優化方式。

官方屬性很少,常用的只有「deprecated」。我們也可以使用非官方的屬性,需要加上名字空間限定。

static_assert 是「靜態斷言」,在編譯階段計算常數和型別,如果斷言失敗就會導致編譯錯誤。它也是邁向模板元程式設計的第一步。

和執行階段的「動態斷言」一樣,static_assert 可以在編譯階段定義各種前置條件,充分利用 c++ 靜態型別語言的優勢,讓編譯器執行各種檢查,避免把隱患帶到執行階段。

靜態屬性和靜態方法2

潛規則 為什麼呢?回顧下 this指標是類的乙個自動生成 自動隱藏的私有成員,它存在於類的非靜態成員函式中,指向被呼叫函式所在的物件的位址。當乙個物件被建立時,該物件的 this指標就自動指向物件資料的首位址。我們從乙個比較有特色的例子來體會 this指標的工作原理 特色例子.txt。include...

php static靜態屬性和靜態方法

一 靜態屬性 靜態屬性不可以由物件通過 操作符來訪問。靜態屬性只能被初始化為文字或常量,不能使用表示式。所以可以把靜態屬性初始化為整數或陣列,但不能初始化為另乙個變數或函式返回值,也不能指向乙個物件。如何訪問靜態屬性呢?在類內部 類名 靜態屬性 或者 self 靜態屬性 在類外部 類名 靜態屬性 或...

靜態庫 a編譯和靜態庫 a合併

第一步 生成test.o目標檔案,使用gcc c test.c o test.o命令。第二步 使用ar將test.o打包成libtest.a靜態庫,使用ar rcs o libtest.a test.o命令 第三步 生成libtest.a靜態庫後,可以使用命令ar t libtest.a檢視libt...