constexpr:編譯期與執行期之間的神祕關鍵字
Scott Meyers在effective modern c++中提到“If there were an award for the most confusing new word in C++11, constexpr would probably win it.”
由此可見,constexpr確實是比較難以讓人理解。加之其在C++11和14中的標準略有不同,也加劇了這種難度。
參考幾本經典教材(C++ primer, effective modern C++, a tour of C++)以及藍色大大在知乎上的一些解答,整理出constexpr的用法和注意事項。
1.概念,constexpr objects
C++ primer中給出的定義是 “常量表達式是指不會改變並且在編譯過程中就能得到計算結果的表示式 【1】。”
可以理解為在const上又加一層限定條件,即const並不限定是編譯期常量還是執行期常量,而constexpr必須是編譯期常量(在編譯階段得到結果)。
舉例如下:
眾所周知,array的size是需要在編譯期確定的,所以當其size不是一個常量表達式時,是無法通過編譯的。
int i;
const int size = i;
int arr[size]; //error,size不是常量表達式,不能在編譯期確定
而如果size是一個constexpr變數,則符合編譯期確定的條件,可以通過編譯。
constexpr auto size = 10;
int arr[size]; //OK,size時常量表達式
當然,要定義一個常量表達式的時候,也要確保其右側是常量表達式,否則該處便無法通過編譯。
int i;
constexpr int size = i; // error,i不能在編譯期確定
所以用effective modern c++中的一句話總結這一部分就是:
“constexpr objects are const and are initialized with values known during compilation【2】”
2. constexpr functions
比起constexpr變數,用constexpr修飾的函式有些更容易混淆的地方。
1) constexpr修飾的函式,當傳入引數是可以在編譯期計算出來時,產生constexpr變數;
當傳入引數不可以在編譯期計算出來時,產生執行期遍歷(constexpr等於不存在)。
因此,不必寫兩個函式,如果函式體存在constexpr適用條件,就應該加上constexpr關鍵字。
例如(例子來源【3】):
constexpr int foo(int i) {
return i + 5;
}
int main() {
int i = 10;
std::array<int, foo(5)> arr; // OK,5是常量表達式,計算出foo(5)也是常量表達式
foo(i); // Call is Ok,i不是常量表達式,但仍然可以呼叫(constexpr 被忽略)
std::array<int, foo(i)> arr1; // Error,但是foo(i)的呼叫結果不是常量表達式了
}
2) 在C++11和14中的區別
在C++11標準中,對於constexpr修飾的函式給了及其苛刻的限定條件:函式的返回值型別及所有形參的型別都是字面值型別,而且函式體內必須有且只有一條return語句【1】。
這個條件顯然是太苛刻了,以至於很多在constexpr的操作都要藉助?:表示式,遞迴等辦法實現。
在C++14中,放寬了這一限定,只保留了“函式的返回值型別及所有形參的型別都是字面值型別”,也就是說,這些值都在編譯期能確定了就行。
3. constexpr class(字面值常量類)
built-in型別是字面值常量,但是有時需要自定義型別也作為字面值常量,這時候就需需要將constexpr修飾建構函式。
字面值常量類必須至少提供一個constexpr建構函式。
例如:
class Point {
public:
constexpr Point(double xval = 0, double yval = 0): x(xval), y(yval) { }
constexpr double getX() const {return x;}
constexpr double getY() const {return y;}
private:
double x,y;
};
當這樣定義一個類後,便可以將Point型別的物件定義為字面值常量。即:
constexpr Point p1(9.4, 27,7);
constexpr Point p2(28.8, 5.3);
constexpr
Point midpoint(const Point& p1, const Point& p2) {
return {p1.getX() + p2.getX() / 2, p1.getY() + p2.getY() / 2} ;
}
constexpr auto mid = midpoint (p1, p2);
上述例子中,p1,p2均為字面值常量,midpoint為constexpr修飾的函式,所以求取mid的整個過程均在編譯期就可以完成,軟體執行的時間自然會大大減少。
至此關於constexpr的三個主要用途(constexpr變數,constexpr修飾函式,constexpr修飾建構函式)就總結完畢,下面是一些注意事項。
注意事項1: 很多人(包括我自己)在gcc中驗證陣列大小必須在編譯期指定的例子時發現:
如果array定義在主函式內,即使給定的不是一個常量表達式,也可以通過編譯。這差點顛覆了我的認知。。。
藍色大大在知乎答案【4】中解釋了這一點,其實是C99中的variable length array。在全域性變數中不能使用(無法分配記憶體),在區域性變數中可以使用,細節可以參考那份解答。
注意事項2:constexpr這麼複雜,到底為什麼要用?
其實第一還是為了效率。效率是C++的設計哲學之一,編譯期可以確定的東西,便可以提醒編譯期優化,也可能存放在read-only memory中
第二就是這樣宣告的constexpr變數便可以用在諸如上述陣列長度指定,還有包括模板引數,case標籤等場合,會便於使用【5】。
參考資料:
1. Stanley B. Lippman / Josée Lajoie / Barbara E. Moo, C++ Primer 中文版(第 5 版)[M]. 電子工業出版社,2013
2. Meyers S. Effective Modern C++[M]. O'Reilly, 2014.
5. Stroustrup B. A Tour of C++[M]. Addison-Wesley Longman, Amsterdam, 2013.