1. C++常量運算式
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,它是變數而非常量,因此不是一個常量運算式,無法用于表示陣列的長度,
我們知道,C++ 程式的執行程序大致要經歷編譯、鏈接、運行這 3 個階段,而常量運算式和非常量運算式的計算時機不同,非常量運算式只能在程式運行階段計算出結果;而常量運算式的計算往往發生在程式的編譯階段,這可以極大提高程式的執行效率,因為運算式只需要在編譯階段計算一次,節省了每次程式運行時都需要計算一次的時間,
對于用 C++ 撰寫的程式,性能往往是永恒的追求,那么在實際開發中,如何才能判定一個運算式是否為常量運算式,進而獲得在編譯階段即可執行的“特權”呢?除了人為判定外,C++11 標準還提供有 constexpr 關鍵字,constexpr 關鍵字的功能是使指定的常量運算式獲得在程式編譯階段計算出結果的能力,而不必等到程式運行階段,C++ 11 標準中,constexpr 可用于修飾普通變數、函式(包括模板函式)以及類的建構式,
注意:獲得在編譯階段計算出結果的能力,并不代表 constexpr 修飾的運算式一定會在程式編譯階段被執行,具體的計算時機還是編譯器說了算,
2. 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 標準規定,浮點常量運算式在編譯階段計算的精度要至少等于(或者高于)運行階段計算出的精度,
3. constexpr修飾函式
constexpr 還可以用于修飾函式的回傳值,這樣的函式又稱為“常量運算式函式”,但需要注意,constexpr 并非可以修改任意函式的回傳值,一個函式要想成為常量運算式函式,必須滿足如下 4 個條件:
- 整個函式的函式體中,除了可以包含 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 陳述句,
- 該函式必須有回傳值,即函式的回傳值型別不能是 void,舉個例子:
constexpr void display() {
//函式體
}
像上面這樣定義的回傳值型別為 void 的函式,不屬于常量運算式函式,原因很簡單,因為通過類似的函式根本無法獲得一個常量,
- 函式在使用之前,必須有對應的定義陳述句,我們知道,函式的使用分為“宣告”和“定義”兩部分,普通的函式呼叫只需要提前寫好該函式的宣告部分即可(函式的定義部分可以放在呼叫位置之后甚至其它檔案中),但常量運算式函式在使用前,必須要有該函式的定義,舉個例子:
#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() 函式之后,查看編譯器的報錯資訊,
可以看到,普通函式在呼叫時,只需要保證呼叫位置之前有相應的宣告即可;而常量運算式函式則不同,呼叫位置之前必須要有該函式的定義,否則會導致程式編譯失敗,
- 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 修改函式時,函式本身也是支持遞回的,
4. 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 的成員方法,
5. 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 型別,實體化后的函式符合常量運算式函式的要求,所以該函式的回傳值就是一個常量運算式,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/540978.html
標籤:其他
上一篇:乾坤大挪移,如何將同步阻塞(sync)三方庫包轉換為異步非阻塞(async)模式?Python3.10實作。
下一篇:每日演算法之和為S的連續正數序列
