constexpr
是 C++11 對於我們已經熟到透的 const
修飾子的一個加強。 const
大家都知道是代表英文中 constant,常數的意思。代表的是被修飾的變數數值編譯期 (compile-time) 已定,也無法再通過語法修改,任何對於標示為常數的變數的嘗試修改都會造成編譯器報錯。
const int n = 10;
n += 3; // compile error
特別強調通過語法是因為你還是可以通過 buffer overflow 等等不小心寫出來的 bug 或特技表演在運行期 (run-time) 把某個 const 裝飾的變數給炸了。
然而對於一個 const
變數來說,我們正常人類智商都可以很輕鬆的知道,他和另個常數的四則運算表達式都應該也是個編譯期的常數。
就像你今天寫了一個 sq
函式想計算一個數平方。當你傳入一個常數,那麼邏輯上,這個函式的返回值也該是個編譯時期就知道的常數。
int sq(int N) {
return N * N;
}const int N = 123;
const int SQ_N = sq(N);
printf("%d %d\n", N, SQ_N);
像上面這樣就很抱歉了。他能算出個值初始化 SQ_N
,但是卻是發生在運行期而不是編譯期。可以通過 g++ a.cpp -S
編出 assembly 查看的確呼叫了 sq
並且把 123 傳進去計算。(這裡不考慮有些編譯器很屌 -O2
優化上面 sq
的計算還有 SQ_N
的初始化在編譯期直接算完)
這也就是 constexpr
所想滿足的語意:常數表達式 (constant expression)。一堆常數可以在編譯時期經過固定確定運算得到確切值的表達式。除了可以在變數掛上 constexpr
,甚至可以在函式的返回值宣告也可以加上 constexpr
來修飾這個函式變成 constexpr function,讓編譯器在編譯時期就能依照 C++ 標準把能算的都算好。
基本上 C++ 內建型別變數都能掛上 constexpr
, constexpr
的變數也只能被常數表達式 (常數變數的運算,constexpr function) 來初始化。而上面提到的 constexpr function 則有一些特別的限制。直觀上是直接在返回值前面掛上 constexpr
就好。
constexpr int sq(int n)
{
return n * n;
}int main()
{
constexpr int N = 123;
constexpr int N_SQ = sq(N); printf("%d %d\n", N, N_SQ);
}
通過 g++ a.cpp -S
我們查看 assembly 發現的確在編譯時期就計算出來 123 的平方 15129 了。
...
movl $123, %esi
movl $15129, %edx ## imm = 0x3B19
movl $123, -4(%rbp)
movl $15129, -8(%rbp) ## imm = 0x3B19
...
顯而易見 constexpr function 有非常硬的限制。從傳入的參數到中間的運算流程都必須是編譯期確切知道的,不然編譯器根本沒辦幫法幫你算。
比如一開始 constexpr function 裡面是不能出現如 if
for
這樣的流程控制的,必須一步到位計算結果,函式體中間也不得出現 n++
這類的表達式,也不能宣告變數。
你可以把 constexpr function 整體想成一個包起來的一行的表達式。一行表達式裡頭不允許你再用假鬼假怪的方式再內嵌一個子表達式。
到了 C++14 之後就解禁大開放, if
可以寫。反正 if 內的關於參數的邏輯陳述只要也是 constexpr statement,他就會幫你編譯期計算。也可以在函式體裡頭宣告你輔助用的變數。也可以寫超過一個 return
敘述。
constexpr int calc(int n)
{
if (n % 2 == 0) { // C++11 compile error
return n * n;
} int a = 10; // C++11 compile error return n * n + a; // C++11 compile error
}int main()
{
constexpr int N = 123;
constexpr int N_SQ = sq(N);
constexpr int N_CALC = calc(N); printf("%d %d %d\n", N, N_SQ, N_CALC); // 123 15129 15139 printf("%d\n", sq(4)); // 編譯期不會計算 sq(4)
}
但反過來說,constexpr function 並不是只能拿來初始化別的 constexpr
變數,在一般使用情境,你也可以直接拿來當作一個執行期的函式來呼叫完全沒有毛病,也體現了 constexpr function 的彈性。
在 enum, switch 的應用
仔細想想,C++ 這兩個語法中, enum
宣告, switch
邏輯判斷分支都需要吃一堆常數。這也是 constexpr
可以發力的地方。下面這個例子,我們利用編譯期計算常數的特性,把一個 FIB_ENUM
列舉的元素宣告成費氏數列的第五、十、二十項。
constexpr int fib(int n)
{
if (n <= 0) {
return 1;
}
return fib(n - 1) + fib(n - 2);
}enum FIB_ENUM {
a = fib(5),
b = fib(10),
c = fib(20),
dummy = fib(0)
};int main()
{
FIB_ENUM my_fib = a;
printf("%d %d %d\n", my_fib, FIB_ENUM::b, FIB_ENUM::c);
// 8 89 10946
}
同樣地, switch
的邏輯分支也可以把各個 case
替換成各種 constexpr
的結果。這裡就不再舉例了。
總結地來說,
constexpr
擴展了原本對於const
的限制,並且明確地給了編譯器更多在編譯時期就可以做的計算空間,讓執行期減少更多不必要的計算。以往需要藉由面目全非的 template meta programming 才能完成的編譯期計算現在可以我們最平常的函式語言就能夠完成。- 也大大地讓程式的語意更完整,不再只有以前 const 那樣子的硬性文法規定,而更加多元地讓「常數」的語義出現在各個地方,讓開發者遵守。
- constexpr function 可以用 macro 函式來想像,但是避掉了非常多使用 macro 的困擾,比如定義的展開結尾分號爆炸,忘記對參數加上括號導致不同優先級的運算子攪和在一起…。
- 更加彈性地,不只是編譯時期的語義遵守和優化。就算當作一般普通函式在別的上下文呼叫也是完全 OK,完全體現出一套語法但依情境做不同事情。
不只是上面提到的用法 constexpr 還有一些巧妙的應用,下期分曉~