C++中的constexpr的意义与用法

以下内容转载medium, 字体为繁体, 不喜欢勿入, 无奈许多博客的介绍都写的太差, 这位台湾小哥是看过写的最清晰最棒的, 由简入深 !

内容转载https://medium.com/@tjsw/%E6%BD%AE-c-constexpr-constructor-constexpr-operator-overloading-3a11062900ff


Part1

近代的 C++ 中為我們傳統熟悉的語句和修飾子多了非常多的元素,今天就來聊聊 C++11 開始引入的 constexpr 修飾子的用法吧。


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++ 內建型別變數都能掛上 constexprconstexpr 的變數也只能被常數表達式 (常數變數的運算,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 的結果。這裡就不再舉例了。


總結地來說,

  1. constexpr 擴展了原本對於 const 的限制,並且明確地給了編譯器更多在編譯時期就可以做的計算空間,讓執行期減少更多不必要的計算。以往需要藉由面目全非的 template meta programming 才能完成的編譯期計算現在可以我們最平常的函式語言就能夠完成。
  2. 也大大地讓程式的語意更完整,不再只有以前 const 那樣子的硬性文法規定,而更加多元地讓「常數」的語義出現在各個地方,讓開發者遵守。
  3. constexpr function 可以用 macro 函式來想像,但是避掉了非常多使用 macro 的困擾,比如定義的展開結尾分號爆炸,忘記對參數加上括號導致不同優先級的運算子攪和在一起…。
  4. 更加彈性地,不只是編譯時期的語義遵守和優化。就算當作一般普通函式在別的上下文呼叫也是完全 OK,完全體現出一套語法但依情境做不同事情。

Part2

constexpr 的用法中我們提過一個 C++ 函式只要滿足一些簡單的限制,現代 C++ 編譯器就可以幫你在編譯期算出函式的結果。而我們一直沒有提到的類別的 constructor 以及類別的 member function 成員函式甚至是 operator overloading 運算子重載,他也是一個函式啊!是不是也能加上 constexpr 修飾呢?

答案當然是可以,不然這篇就不用寫了。

那麼具體來說 constexpr 加在類別的成員函式們可以幹嘛?就是希望我們開發 者自定義的類別也可以宣告成 constexpr 變數,達到編譯期的運算效果。

#include <cstdio>constexpr int sq(int n) { return n * n; }constexpr CClass cube(const CClass &c) { return c * c * c; }int main()
{
constexpr CClass c1(sq(2));
constexpr CClass c2_6 = cube(c1);
constexpr CClass c3_6 = cube(CClass(sq(3)));
static_assert(c2_6.n_ == 64, “!!!”);
static_assert(c3_6.n_ == 729, “!!!”);
}

如果一切順利的話,我們應該要可以算出 c2_6 代表 2 的 6 次方,c3_6 代表 3 的 6 次方,並且最後通過 printf 印出來。

我們觀察一下上面的半殘代碼:

  1. 上面的 cube() 涉及了對 CClass 物件的「乘法」,很明顯這裡我們需要實作 CClass 的乘法 operator overloading。
  2. cube() 還是一個標上 constexpr 的函式,代表了裡面的運算元 ©,以及運算 () 都要是編譯時期可知的操作,也就是 c * c * c 必須是一個 constant expression
  3. c1 也掛上 constexpr,因此他的初始化操作也必須是一個 constexpr function。

結上所述,我們必須對 CClass 的 constructor,以及 operator() 加上 constexpr 的修飾。

一個 constructor 畢竟代表了整個類別的性質,掛上 constexpr 的 constructor 就代表使用者自定義的類別是一個字面型別 (literal type)。

因此整個類別被拿用初始化 constexpr 變數時 (例如 main()c1) ,必須確保除了 constexpr constructor 裡面沒有奇怪的操作,而且所有的成員變數都要被初始化列表 (member initialization list) 以編譯時期可知的值初始化。(或是成員宣告直接賦值也行)

完整的 CClass 定義應該是這樣的:

class CClass
{
public:
constexpr explicit CClass(int n) : n_(n) {}
constexpr CClass operator*(const CClass &rhs) const {
return CClass(n_ * rhs.n_);
}
int n_;
};
  1. CClass 的 constructor 完全沒有操作,也初始化了所有,也是唯一的成員變數 n_,因此自然就符合了 constexpr 的限制。(可以試試看把 n_ 寫在 constructor 函式體裡面賦值,就會吃 compile error 了)
  2. operator*() 就是個成員函式,做了一個 constexpr 物件回來,也很乖巧。

所以我們再度利用組語編譯,也能發現編譯器的確把 c2_6c3_6 在編譯時期就算出來了。

.section __TEXT,__const
.p2align 2 ## @_ZZ4mainE2c1
__ZZ4mainE2c1:
.long 4 ## 0x4
.p2align 2 ## @ZZ4mainE4c2_6
ZZ4mainE4c2_6:
.long 64 ## 0x40
.p2align 2 ## @ZZ4mainE4c3_6
ZZ4mainE4c3_6:
.long 729 ## 0x2d9

也可以不要編譯期算

constexpr CClass c2_6 = cube(c1);
constexpr CClass c3_6 = cube(CClass(sq(3)));
CClass c88(88);
static_assert(c2_6.n == 64, “!!!”);
static_assert(c3_6.n == 729, “!!!”);
printf("%d %d %d\n", c2_6.n, c3_6.n, c88.n);

就像 constexpr function 一樣,根據我們在什麼時候使用,就可以編譯期算,或是執行期才算。比如上面這個 c88,他就不會是一個在 TEXT 段的常數。而是執行期呼叫 printf() 的時候才把 n 從 register 裡面拿出來壓進 stack 裡呼叫 printf()

今天的內容很簡單,就是再把 constexpr 的適用範圍再擴展

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值