C++11 constexpr:驗證是否爲常量表達式(長篇神文)

constexpr 是 C++ 11 標準新引入的關鍵字,不過在講解其具體用法和功能之前,讀者需要先搞清楚 C++ 常量表達式的含義。

所謂常量表達式,指的就是由多個(≥1)常量組成的表達式。換句話說,如果表達式中的成員都是常量,那麼該表達式就是一個常量表達式。這也意味着,常量表達式一旦確定,其值將無法修改。

實際開發中,我們經常會用到常量表達式。以定義數組爲例,數組的長度就必須是一個常量表達式:



// 1)int url[10];//正確// 2)int url[6 + 4];//正確// 3)int length = 6;int url[length];//錯誤,length是變量

上述代碼演示了 3 種定義 url 數組的方式,其中第 1、2 種定義 url 數組時,長度分別爲 10 和 6+4,顯然它們都是常量表達式,可以用於表示數組的長度;第 3 種 url 數組的長度爲 length,它是變量而非常量,因此不是一個常量表達式,無法用於表示數組的長度。

常量表達式的應用場景還有很多,比如匿名枚舉、switch-case 結構中的 case 表達式等,感興趣的讀者可自行編碼測試,這裏不再過多舉例。

我們知道,C++ 程序的執行過程大致要經歷編譯、鏈接、運行這 3 個階段。值得一提的是,常量表達式和非常量表達式的計算時機不同,非常量表達式只能在程序運行階段計算出結果;而常量表達式的計算往往發生在程序的編譯階段,這可以極大提高程序的執行效率,因爲表達式只需要在編譯階段計算一次,節省了每次程序運行時都需要計算一次的時間。

對於用 C++ 編寫的程序,性能往往是永恆的追求。那麼在實際開發中,如何才能判定一個表達式是否爲常量表達式,進而獲得在編譯階段即可執行的“特權”呢?除了人爲判定外,C++11 標準還提供有 constexpr 關鍵字。

constexpr 關鍵字的功能是使指定的常量表達式獲得在程序編譯階段計算出結果的能力,而不必等到程序運行階段。C++ 11 標準中,constexpr 可用於修飾普通變量、函數(包括模板函數)以及類的構造函數。



注意,獲得在編譯階段計算出結果的能力,並不代表 constexpr 修飾的表達式一定會在程序編譯階段被執行,具體的計算時機還是編譯器說了算。

constexpr修飾普通變量

C++11 標準中,定義變量時可以用 constexpr 修飾,從而使該變量獲得在編譯階段即可計算出結果的能力。

值得一提的是,使用 constexpr 修改普通變量時,變量必須經過初始化且初始值必須是一個常量表達式。舉個例子:

#include <iostream>using namespace std;int main(){    constexpr int num = 1 + 2 + 3;    int url[num] = {1,2,3,4,5,6};    couts<< url[1] << endl;    return 0;}

程序執行結果爲:

2

讀者可嘗試將 constexpr 刪除,此時編譯器會提示“url[num] 定義中 num 不可用作常量”。

可以看到,程序第 6 行使用 constexpr 修飾 num 變量,同時將 "1+2+3" 這個常量表達式賦值給 num。由此,編譯器就可以在編譯時期對 num 這個表達式進行計算,因爲 num 可以作爲定義數組時的長度。

有讀者可能發現,將此示例程序中的 constexpr 用 const 關鍵字替換也可以正常執行,這是因爲 num 的定義同時滿足“num 是 const 常量且使用常量表達式爲其初始化”這 2 個條件,由此編譯器會認定 num 是一個常量表達式。

注意,const 和 constexpr 並不相同,關於它們的區別,我們會在下一節做詳細講解。

另外需要重點提出的是,當常量表達式中包含浮點數時,考慮到程序編譯和運行所在的系統環境可能不同,常量表達式在編譯階段和運行階段計算出的結果精度很可能會受到影響,因此 C++11 標準規定,浮點常量表達式在編譯階段計算的精度要至少等於(或者高於)運行階段計算出的精度。

constexpr修飾函數

constexpr 還可以用於修飾函數的返回值,這樣的函數又稱爲“常量表達式函數”。

注意,constexpr 並非可以修改任意函數的返回值。換句話說,一個函數要想成爲常量表達式函數,必須滿足如下 4 個條件。

1) 整個函數的函數體中,除了可以包含 using 指令、typedef 語句以及 static_assert 斷言外,只能包含一條 return 返回語句。

舉個例子:





constexpr int display(int x) {    int ret = 1 + 2 + x;    return ret;}

注意,這個函數是無法通過編譯的,因爲該函數的返回值用 constexpr 修飾,但函數內部包含多條語句。


如下是正確的定義 display() 常量表達式函數的寫法:

constexpr int display(int x) {    //可以添加 using 執行、typedef 語句以及 static_assert 斷言    return 1 + 2 + x;}

可以看到,display() 函數的返回值是用 constexpr 修飾的 int 類型值,且該函數的函數體中只包含一個 return 語句。


2) 該函數必須有返回值,即函數的返回值類型不能是 void。

舉個例子:


constexpr void display() {    //函數體}

像上面這樣定義的返回值類型爲 void 的函數,不屬於常量表達式函數。原因很簡單,因爲通過類似的函數根本無法獲得一個常量。


3) 函數在使用之前,必須有對應的定義語句。我們知道,函數的使用分爲“聲明”和“定義”兩部分,普通的函數調用只需要提前寫好該函數的聲明部分即可(函數的定義部分可以放在調用位置之後甚至其它文件中),但常量表達式函數在使用前,必須要有該函數的定義。

舉個例子:


#include <iostream>using namespace std;//普通函數的聲明int noconst_dis(int x);//常量表達式函數的聲明constexpr int display(int x);//常量表達式函數的定義constexpr int display(int x){    return 1 + 2 + x;}int main(){    //調用常量表達式函數    int a[display(3)] = { 1,2,3,4 };    cout << a[2] << endl;    //調用普通函數    cout << noconst_dis(3) << endl;    return 0;}//普通函數的定義int noconst_dis(int x) {    return 1 + 2 + x;}

程序執行結果爲:

3
6

讀者可自行將 display() 常量表達式函數的定義調整到 main() 函數之後,查看編譯器的報錯信息。

可以看到,普通函數在調用時,只需要保證調用位置之前有相應的聲明即可;而常量表達式函數則不同,調用位置之前必須要有該函數的定義,否則會導致程序編譯失敗。

4) return 返回的表達式必須是常量表達式,舉個例子:

#include <iostream>using namespace std;int num = 3;constexpr int display(int x){    return num + x;}int main(){    //調用常量表達式函數    int a[display(3)] = { 1,2,3,4 };    return 0;}
該程序無法通過編譯,編譯器報“display(3) 的結果不是常量”的異常。

常量表達式函數的返回值必須是常量表達式的原因很簡單,如果想在程序編譯階段獲得某個函數返回的常量,則該函數的 return 語句中就不能包含程序運行階段才能確定值的變量。

注意,在常量表達式函數的 return 語句中,不能包含賦值的操作(例如 return x=1 在常量表達式函數中不允許的)。另外,用 constexpr 修改函數時,函數本身也是支持遞歸的,感興趣的讀者可自行嘗試編碼測試。

constexpr修飾類的構造函數

對於 C++ 內置類型的數據,可以直接用 constexpr 修飾,但如果是自定義的數據類型(用 struct 或者 class 實現),直接用 constexpr 修飾是不行的。

舉個例子:

#include <iostream>using namespace std;//自定義類型的定義constexpr struct myType {    const char* name;    int age;    //其它結構體成員};int main(){    constexpr struct myType mt { "zhangsan", 10 };    cout << mt.name << " " << mt.age << endl;    return 0;}

此程序是無法通過編譯的,編譯器會拋出“constexpr不能修飾自定義類型”的異常。


當我們想自定義一個可產生常量的類型時,正確的做法是在該類型的內部添加一個常量構造函數。例如,修改上面的錯誤示例如下:

#include <iostream>using namespace std;//自定義類型的定義struct myType {    constexpr myType(char *name,int age):name(name),age(age){};    const char* name;    int age;    //其它結構體成員};int main(){    constexpr struct myType mt { "zhangsan", 10 };    cout << mt.name << " " << mt.age << endl;    return 0;}

程序執行結果爲:

zhangsan 10

可以看到,在 myType 結構體中自定義有一個構造函數,藉助此函數,用 constexpr 修飾的 myType 類型的 my 常量即可通過編譯。

注意,constexpr 修飾類的構造函數時,要求該構造函數的函數體必須爲空,且採用初始化列表的方式爲各個成員賦值時,必須使用常量表達式。

前面提到,constexpr 可用於修飾函數,而類中的成員方法完全可以看做是“位於類這個命名空間中的函數”,所以 constexpr 也可以修飾類中的成員函數,只不過此函數必須滿足前面提到的 4 個條件。

舉個例子:





#include <iostream>using namespace std;//自定義類型的定義class myType {public:    constexpr myType(const char *name,int age):name(name),age(age){};    constexpr const char * getname(){        return name;    }    constexpr int getage(){        return age;    }private:    const char* name;    int age;    //其它結構體成員};int main(){    constexpr struct myType mt { "zhangsan", 10 };    constexpr const char * name = mt.getname();    constexpr int age = mt.getage();    cout << name << " " << age << endl;    return 0;}

程序執行結果爲:

zhangsan 10

注意,C++11 標準中,不支持用 constexpr 修飾帶有 virtual 的成員方法。

constexpr修飾模板函數

C++11 語法中,constexpr 可以修飾模板函數,但由於模板中類型的不確定性,因此模板函數實例化後的函數是否符合常量表達式函數的要求也是不確定的。

針對這種情況下,C++11 標準規定,如果 constexpr 修飾的模板函數實例化結果不滿足常量表達式函數的要求,則 constexpr 會被自動忽略,即該函數就等同於一個普通函數。

舉個例子:



#include <iostream>using namespace std;//自定義類型的定義struct myType {    const char* name;    int age;    //其它結構體成員};//模板函數template<typename T>constexpr T dispaly(T t){    return t;}int main(){    struct myType stu{"zhangsan",10};    //普通函數    struct myType ret = dispaly(stu);    cout << ret.name << " " << ret.age << endl;    //常量表達式函數    constexpr int ret1 = dispaly(10);    cout << ret1 << endl;    return 0;}

程序執行結果爲:

zhangsan 10
10

可以看到,示例程序中定義了一個模板函數 display(),但由於其返回值類型未定,因此在實例化之前無法判斷其是否符合常量表達式函數的要求:

  • 第 20 行代碼處,當模板函數中以自定義結構體 myType 類型進行實例化時,由於該結構體中沒有定義常量表達式構造函數,所以實例化後的函數不是常量表達式函數,此時 constexpr 是無效的;

  • 第 23 行代碼處,模板函數的類型 T 爲 int 類型,實例化後的函數符合常量表達式函數的要求,所以該函數的返回值就是一個常量表達式。

圖片


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章