1. 程式人生 > 其它 >編譯階段能做什麼:屬性和靜態斷言

編譯階段能做什麼:屬性和靜態斷言

屬性(attribute)

屬性“deprecated”,用來標記不推薦使用的變數、函式或者類,也就是被“廢棄”。
比如說,你原來寫了一個函式 old_func(),後來覺得不夠好,就另外重寫了一個完
全不同的新函式。但是,那個老函式已經發布出去被不少人用了,立即刪除不太可能,
該怎麼辦呢?
這個時候,你就可以讓“屬性”發揮威力了。你可以給函式加上一個“deprecated”的
編譯期標籤,再加上一些說明文字:

[[deprecated("deadline:2020-12-31")]] // C++14 or later
int old_func();

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

 warning: ‘int old_func()’ is deprecated: deadline:2020-12-31 [-Wdeprecated-decl

“屬性”也支援非標準擴充套件,允許以類似名字空間的方式使用編譯器自己的一些“非官
方”屬性,比如,GCC 的屬性都在“gnu::”裡。下面我就列出幾個比較有用的.

deprecated:與 C++14 相同,但可以用在 C++11 裡。
unused:用於變數、型別、函式等,表示雖然暫時不用,但最好保留著,因為將來可能
會用。
constructor:函式會在 main() 函式之前執行,效果有點像是全域性物件的建構函式。
destructor:函式會在 main() 函式結束之後執行,有點像是全域性物件的解構函式。
always_inline:要求編譯器強制行內函數,作用比 inline 關鍵字更強。
hot:標記“熱點”函式,要求編譯器更積極地優化。

[[gnu::unused]] // 宣告下面的變數暫不使用,不是錯誤
int nouse;

靜態斷言(static_assert)

assert屬於動態斷言,用來斷言一個表示式必定為真。比如說,數字必須是正數,指標
必須非空、函式必須返回 true,當程式(也就是 CPU)執行到 assert 語句時,就會計
算表示式的值,如果是 false,就會輸出錯誤訊息,然後呼叫 abort() 終止程式的執行。

assert(i > 0 && "i must be greater than zero");
assert(p != nullptr);
assert(!str.empty());

有了“動態斷言”,那麼相應的也就有“靜態斷言”,名字也很像,叫“static_assert”,不
過它是一個專門的關鍵字,而不是巨集。因為它只在編譯時生效,執行階段看不見,所以是
“靜態”的。如下斐波拉契數列計算函式,可以用靜態斷言來保證模板引數必須大於等於零:

template<int N>
struct fib
{
static_assert(N >= 0, "N >= 0");
static const int value =
fib<N - 1>::value + fib<N - 2>::value;
};

再比如說,要想保證我們的程式只在 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."); // 錯誤用法

說到這兒,你大概對 static_assert 的“編譯計算”有點感性認識了吧。在用“靜態斷
言”的時候,你就要在腦子裡時刻“繃緊一根弦”,把自己代入編譯器的角色,像編譯器那
樣去思考,看看斷言的表示式是不是能夠在編譯階段算出結果。不過這句話說起來容易做
起來難,計算數字還好說,在泛型程式設計的時候,怎麼檢查模板型別呢?比如說,斷言是整
數而不是浮點數、斷言是指標而不是引用、斷言型別可拷貝可移動……
這些檢查條件表面上看好像是“不言自明”的,但要把它們用 C++ 語言給精確地表述出
來,可就沒那麼簡單了。所以,想要更好地發揮靜態斷言的威力,還要配合標準庫裡的
“type_traits”,它提供了對應這些概念的各種編譯期“函式”。

// 假設T是一個模板引數,即template<typename T>
static_assert(
is_integral<T>::value, "int");
static_assert(
is_pointer<T>::value, "ptr");
static_assert(
is_default_constructible<T>::value, "constructible");

你可能看到了,“static_assert”裡的表示式樣子很奇怪,既有模板符號“<>”,又有作
用域符號“::”,與執行階段的普通表示式大相徑庭,初次見到這樣的程式碼一定會嚇一跳。
這也是沒有辦法的事情。因為 C++ 本來不是為編譯階段程式設計所設計的。受語言的限制,編
譯階段程式設計就只能“魔改”那些傳統的語法要素了:把類當成函式,把模板引數當成函式參
數,把“::”當成 return 返回值。說起來,倒是和“函數語言程式設計”很神似,只是它執行在
編譯階段。

小結

1.“屬性”相當於編譯階段的“標籤”,用來標記變數、函式或者類,讓編譯器發出或者
不發出警告,還能夠手工指定程式碼的優化方式。
2.官方屬性很少,常用的只有“deprecated”。我們也可以使用非官方的屬性,需要加上
名字空間限定。
3.static_assert 是“靜態斷言”,在編譯階段計算常數和型別,如果斷言失敗就會導致編
譯錯誤。它也是邁向模板超程式設計的第一步。
4.和執行階段的“動態斷言”一樣,static_assert 可以在編譯階段定義各種前置條件,充
分利用 C++ 靜態型別語言的優勢,讓編譯器執行各種檢查,避免把隱患帶到執行階段。