C++ const 大亂鬥:const、constexpr、consteval、constinit

| | 0 Comments| 10:16
Categories:

C++20 裡,以 const 為字首的關鍵字總共有四個:constconstexprconstevalconstinit;其中,const 應該是大家都熟系的老標準就有的,constexpr 則是在 C++11 的時候加入的;constevalconstinit 則是 C++20 的新東西。

這四種 const 到底差別是什麼呢?這邊參考《const vs constexpr vs consteval vs constinit in C++20》這篇文章、稍微整理一下這四種 const 的差別。


const

最基本的 const,代表的是「常數」(constant),代表是不可修改的;下面就是一個簡單的例子:

const int a = 1;
a = 2; // error

因為變數 a 在宣告時有加上 const,所以之後都不能去修改他的值。

而另一種用法,則是用在成員函式上,禁止修改成員資料;不過這邊就先跳過了。

雖然 const 代表這個值不會有變化,但是它不代表變數會是在編譯階段(compile-time)就完成處理,所以雖然在整數的時候可以當作編譯階段的變數、但是其他型別的變數的時候就不行了。

下面就是一個例子:

const int count = 3;
std::array<double, count> doubles{ 1.1, 2.2, 3.3 };

// but not double:
const double dCount = 3.3;
std::array<double, static_cast<int>(dCount)> moreDoubles{ 1.1, 2.2, 3.3 };
// error: the value of 'dCount' is not usable in a constant expression

前面的 count 雖然可以拿來作為 array 的 template 引數,但是後面的 static_cast<int>(dCount) 卻會出現編譯錯誤。

像是這種要求在編譯階段就有明確的值、或是結果的式子,在 C++ 裡面是被稱為「constant expressions」(參考),最常見的就是這種 non-type template arguments、或是陣列的大小。


constexpr

constexpr 是在 C++11 時加入的(CppReference),它代表的是宣告的變數或函式是可以在編譯階段完成計算的。

像上面的例子就可以修改成:

constexpr double dCount = 3.3;
std::array<double, static_cast<int>(dCount)> doubles2{ 1.1, 2.2, 3.3 };

這樣就可以正確編譯了。

而如果把它用在函式上的話,則會變成一個可以在編譯階段(compile-time)執行、也可以在執行階段(runtime)執行的函式。

像在下面的例子裡面,square() 這個函式就可以同時在 compile-time 和 runtime 的狀況下執行:

#include <iostream>
#include <array>
 
constexpr int square(const int val)
{
  return val * val;
}
 
int main(int argc, char** argv)
{
  // compile time
  const int count = 3;
  std::array<double, square(2)> doubles;
 
  // runtime
  int v = 3;
  int r = square(v);
}

但是這邊也要注意,如果一個函式被標記是 constexpr 的話,那也需要確定它的內容都能在編譯階段就被計算完;如果裡面有用到不是在編譯階段就決定的內容的話,那在惠沒辦法在編譯階段執行的。

下面就是一個例子:

#include <iostream>
#include <array>
 
int i = 10;
 
constexpr int get_val(const int val)
{
  return val * i;
}
 
int main(int argc, char** argv)
{
  std::array<int, get_val(2)> a; //error
}

上面的例子裡面,由於 get_val() 有用到 i 這個一般的全域變數,所以無法在編譯階段完成,這也導致在宣告 std::array<> 的時候會出現編譯錯誤。

但是由於 constexpr 也允許有 runtime 的版本,所以如果是去呼叫他的 runtime 版本、像是 int v = get_val(2) 的話,則是可以正確編譯的。

而實際上,Heresy 之前在《在 header 檔使用 constexpr 定義全域變數》這篇文章也有簡單介紹過,constexpr 也還可以拿來更方便地定義全域變數,有興趣的話也可以看看。

此外,if constexpr() 這種在編譯階段就完成的條件判斷,在很多時候也是很好用的!

所以如果要大量使用 template 這類在編譯階段就決定數值的寫法的話,那 constexpr 會是相當有幫助的!


consteval

consteval 是在 C++20 加入的(CppReference),他和 constexpr 的不同在於他只能用來宣告函式、不能用在變數上;同時,他會明確地限制函式僅能在編譯階段執行。

像上面的例子,如果把 square() 前面的 constexpr 改成 consteval 的話,就是下面的樣子:

#include <iostream>
#include <array>
 
consteval int square(const int val)
{
  return val * val;
}
 
int main(int argc, char** argv)
{
  // compile time
  const int count = 3;
  std::array<double, square(2)> doubles;
 
  // runtime
  int v = 3;
  int r = square(v);  // error
}

這樣的話,「int r = square(v);」就因為 v 不是 compile-time 就決定的變數、而會出現編譯錯誤了。


constinit

constinit 也是在C++20 加入的(CppReference);相較於 consteval 只能用在函式上,constinit 則是只能用在靜態(static)和 thread-local 變數上,它的意義是強制變數要在編譯階段完成初始化(constant initialization、參考)。

而比較有趣的是,他雖然是以「const」開頭,但在使用他宣告變數的時候,實際上並沒有 const 不可修改的特性。

下面就是一個例子:

#include <iostream>
 
constexpr int square(const int val)
{
  return val * val;
}
 
constinit int v = square(2);
 
int main(int argc, char** argv)
{
  v = 3;
}

也因為這邊的 v 不是常數,所以儘管它是在編譯階段就完成初始化的,但是也不能拿來作為 template 引數;像是這邊如果寫 std::array<int, v> 就會出現編譯錯誤。

而如果希望變數有 const 不可修改的特性的話,應該就是直接回去用 constexpr 就可以了。


大概就是這樣了,要整理的話,大概就是:

  • const:可用在變數、成員函式,代表不可修改、但是不保證是編譯階段完成
  • constexpr:可用在變數、函式,代表不可修改、可以在編譯階段完成,另外還可以搭配 if 使用
  • consteval:只可用在函式,代表函式必須要在編譯階段完成
  • constinit:只可用在 static 和 thread-local 變數、代表變數要在編譯階段完成初始化,變數之後可以修改。

不過老實說,個人是不知道 constinit 什麼時候有用啦…

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *