摘要訊息 : C++ 中的容易產生疑惑的三個關鍵字 : constexpr, constevalconstinit.

0. 前言

C++ 11 引入了 constexpr, 自此 C++ 走上了 constexpr everything 的道路. 我們最開始在《C++ 學習筆記》中介紹了 constexpr 在變數和函式上的特性. 然後我們又在《【C++】細談 constexpr中又詳細解釋了一下 constexpr. 當然, 並不只是這些. 在一些 Proposal 導讀的文章中, 也涉及到了 C++ 14 和 C++ 17 中關於 constexpr 的改變. 本篇文章會重新對 constepxr 進行介紹, 並且幫助大家重新認識 constexpr, 同時介紹 C++ 20 引入的兩個新的關鍵字 constevalconstinit.

本文的目錄中的標題過長, 可能影響前面章節的閱讀體驗, 故本篇文章的目錄預設為隱藏不展開狀態, 需要閣下手動展開.

更新紀錄 :

  • 2022 年 6 月 16 日進行第一次更新和修正.

1. constexpr

constexpr 作為一個標識符, 有兩種作用 :

  • constexpr 用於標識函式的時候, 表示函式有能力在編碼期進行計算. 當函式確實可以在編碼期進行計算的時候, 編碼器就會在編碼期完成函式的運作, 提升了程式碼效能; 如果編碼器無法在編碼期完成函式的運作, 函式才會轉而在運作期被運作;
  • constexpr 用於標識變數的時候, 表示變數是一個常數表達式, 並且其值必定可以在編碼期取得.

也就是說, constexpr 在函式和變數上的效果是不一樣的, 特別是要區分函式上的 "有能力" 和變數上的 "必定".

在變數上, 帶有 constexpr 標識的變數會自動帶有 const 限定; 在函式上, constexpr 標識會讓函式自動帶有 inline 標識.

例題 1. 分析一段關於 constexpr 的程式碼.

#include <iostream>

constexpr int func(int x) noexcept {
    return x;
}

int main(int argc, char *argv[]) {
    constexpr auto i {func(42)};     // 函式 func 在編碼期計算
    int input;
    std::cin >> input;
    auto input_to_func {func(input)};       // 不會產生編碼錯誤, 函式 func 變為運作期計算
    constexpr auto i2 {input};      // Error : 帶有 constexpr 的變數的值必須是常數表達式, 而 input 並不是
}
:

如果大家學習過匯編語言, 那麼可以很直觀地從匯編中看到變數 i 的初始化是否在編碼期進行. 我這裡擷取了 Clang 編碼器的一段匯編 :

Figure 1. Code 1 對應的匯編程式碼

如果變數 i 的初始化是在運作期, 那麼應該會出現呼叫兩次 func. 但是我們可以清楚地看到, func 這個名稱在上面的匯編程式碼中只出現了一次, 即黑色框內的 call func(int). 另外, 數字 42 出現在了紅色框內, 但是並不是以 call 的形式, 而是以 mov 指令出現的. 這裡給大家介紹一下, mov 指令是直接給記憶體的某個部分或者暫存器的某個部分指派一個值. 也就是說, 編碼期直接就把 42 指派給了記憶體的某個部分, 並沒有呼叫函式 func. 於是, 我們可以確定變數 i 的初始化必定在編碼期.

\blacksquare

需要注意的是, C++ 11 中帶有 constexpr 限定的函式, 其限制是非常嚴格的 :

  • typedef 宣告;
  • using 宣告;
  • static_assert 陳述式;
  • 空的陳述式;
  • 回傳型別不能為 void 型別;

也就是說, 除了一個 return 陳述式之外, 幾乎不能有其它陳述式.

1.1 C++ 14 中的 constexpr

C++ 14 緩和了 C++ 11 中對 constexpr 函式的限制. 因為例如 while(k--) {++x;} 這種程式碼確實也可以在編碼期完成計算. 其中, xk 都是 constexpr 函式的參數. 因此, C++ 14 提案 N3652《Relaxing constraints on constexpr functions》constexpr 進行了改變 :

  • 允許 constexpr 函式的回傳值為 void (void 被更改為字面值型別);
  • 函式內部不出現 goto 陳述式, try 區塊, 未初始化的變數, 靜態變數和帶有 thread_local 標識的變數, 只要變數的值是字面值即可;
  • 函式內變數的值可以改變, 但是這個變數的可視範圍不能超過函式之外.

1.2 C++ 17 中的 constexpr

C++ 17 對 constexpr 沒有大的改變, 不過為 constexpr 引入了兩個新的特性 : 允許 Lambda 表達式標識 constexpr並且加入了新的條件判斷陳述式 if constexpr.

在 C++ 17 之前, constexpr 函式內是不能出現 Lambda 表達式的, Lambda 表達式也不允許被 constexpr 標識, 因為 Lambda 表達式的型別不屬於字面值型別. 然而實際上, Lambda 表達式是一個函式, 不允許為其標識 constexpr 是不合理的. 因此, C++ 17 提案 P0170R1《Wording for Constexpr Lambda》就提出 :

  • 允許 constexpr 函式內部出現 Lambda 表達式;
  • 允許 Lambda 表達式被 constexpr 標識 : constexpr 可以出現在 Lambda 表達式宣告處 (auto 之前, 類似於為變數的標識方法), 也可以標識在 Lambda 表達式的參數列表之後, 尾置回傳型別之前;
  • 允許沒有被 constexpr 標識的 Lambda 表達式在編碼期被呼叫 :
    auto f {[](int x) -> int {return x + 42;}};
    constexpr auto i {f(1)};        // OK
    auto f2 {[](int x) constexpr -> int {return 42;}};      // OK
  • 允許帶有 constexpr 的函式指標指向 Lambda 表達式, 使得通過該函式指標呼叫的 Lambda 表達式有編碼期計算能力 :
    auto f {[](int x) -> int {return x + 42;}}
    constexpr int (*p)(int) {f};     // OK
    constexpr auto i {p(1)};        // OK, i 的值為 43

C++ 17 提案 P0292R2《constexpr if: A slightly different syntax》還提出編碼期條件判斷陳述式 if constexpr. 一般來說, if constexpr 一般是提供給程式庫作者使用的 :

#include <type_traits>

template <typename T>
class simple_vector {
private:
    T *start;
    size_t size;
    size_t used;
public:
    simple_vector(size_t size, const T &value) : start {(T *)::operator new(size * sizeof(T))}, size {size}, used {size} {
        if constexpr(std::is_nothrow_copy_constructible_v<T>) {
            // 不考慮例外情況的程式碼
            // ...
        }else {
            try {
                // 考慮例外情況的程式碼
                // ...
            }catch(...) {
                // ...
            }
        }
    }
    // ...
};

if constexpr 中的條件為 true 的時候, 編碼期會把這一區塊中的內容進行編碼, 從而遺棄 else 區塊中的程式碼.

1.3 C++ 20 中的 constexpr

C++ 20 進一步擴大了 constexpr 的可用範圍.

1.3.1 允許 constexpr 函式中進行記憶體配置

讓每一個函式都能 constexpr 貌似是 C++ 的目標, 但是部分函式由於存在記憶體配置, 看似永遠無法讓其有能力在編碼期進行計算. 為了解決這個問題, C++ 20 提案 P0784R7《More constexpr containers》提出了解除這個限制, 也就是允許在 constexpr 進行記憶體配置. 但是, 在 constexpr 進行了記憶體配置之後, 還能否確保函式有能力在編碼期計算呢? 答案是可以. 因為編碼器在進行編碼的時候, 可以檢測部分記憶體配置可以成為常數表達式的一部分. 例如

#include <new>

constexpr void constant_operator(int *) noexcept;
constexpr void f(const int &x) {
    auto p {new (std::nothrow) int {x}};
    if(p) {
        *p = constant_operation(*p);
    }
    delete p;
}

Code 4 中, 對於指標 p 的操作是常數表達式, 但是由於 p 採用了動態記憶體配置, 因此在 C++ 20 之前都會產生編碼錯誤. 實際上, 如果編碼器在編碼期為 p 配置了記憶體, 那麼函式 f 完全有能力在編碼期進行計算. 上面的 p 的型別為 int *, 如果是一個自訂型別是否仍然可行呢? C++ 20 規定, 只要自訂型別的建構和解構都是常數表達式, 那麼使用這些自訂型別替換上面的 new (std::nothrow) T {} 仍然是可行的, 計算還是會在編碼期進行.

1.3.2 允許虛擬函式被 constexpr 標識

事實上, 虛擬函式稱為 constexpr 是可能的, 例如下面程式碼

struct S1 {
    virtual int f() {
        return 0;
    }
};
struct S2 : S1 {
    int f() override {
        return 42;
    }
};

完全有能力在編碼期運作. 只是 C++ 20 之前, C++ 標準欽定了虛擬函式無法被 constexpr 標識, 這些函式被迫只能在運作期被呼叫. C++ 20 提案 P1064R0《Allowing Virtual Function Calls in Constant Expressions》提出了解除這個限制, 讓虛擬函式也可以被 constexpr 標識. 這樣帶來了兩個問題也需要被解決 :

  1. 是否允許一個不帶有 constexpr 標識的虛擬函式被一個帶有 constexpr 標識的函式覆蓋?
  2. 是否允許一個帶有 constexpr 標識的虛擬函式被不帶有 constexpr 標識的函式覆蓋?

這兩個問題的答案都是允許. 不過需要注意, 若一個不帶有 constexpr 標識的函式被一個帶有 constexpr 標識的函式所覆蓋, 那麼基礎類別中所對應的函式不應該是純虛擬函式, 另外重寫的函式中不應該涉及基礎類別的操作. 除此之外, 如果重寫的函式不是 constexpr 的, 但是基礎類別中對應的函式是 constexpr 的, 那麼重寫的函式不再具備常數表達式的性質.

1.3.3 允許 try-catch 區塊出現在 constexpr 函式中

既然都允許在 constexpr 函式中進行記憶體配置, 那麼允許 try-catch 區塊在 constexpr 函式中出現也是比較合理的. 但是此時, try-catch 區塊的處理方式不太相同. C++ 20 提案 P1002R1《Try-catch blocks in constexpr functions》提出的解決方案是如果函式在編碼期進行計算, 並且函式中帶有 try-catch 區塊, 那麼直接忽略例外情況操作. 也就是說, 編碼器在編碼期計算帶有 try-catch 區塊的函式的時候, 會自動忽略掉那些 try-catch 區塊, 就像它們沒有被寫在程式碼中一樣. 從而, catch 區塊中的全部程式碼會被直接忽略. 這樣做是有意義的, 因為既然函式在編碼期進行計算, 那麼所有陳述式都必定帶有常數表達式的性質, 自然也就不會擲出任何例外情況.

不過, P1002R1 明確指出, throw 表達式仍然不允許出現在 constexpr 函式中.

1.3.4 允許 dynamic_casttypeid 出現在 constexpr 函式中

如果你熟悉 C++ 編碼器對虛擬函式的處理, 那麼你自然會知道幾乎所有現代編碼器對虛擬函式的處理都是使用了一張虛擬函式表, 表內存的是虛擬函式的指標, 指向虛擬函式真正要運作的程式碼區域.

例題 2. 簡單解析程式碼

struct S1 {
    virtual int f() {
        return 0;
    }
};
struct S2 : S1 {
    int f() override {
        return 42;
    }
};

在記憶體中的佈局.

:
Figure 2. Code 6 的簡易記憶體佈局

需要提前說明的是, Figure 2 是簡易的說明圖, 編碼器真實的實作方式和上圖有一定差別. 不過, 我們通過 Figure 2 可以看到, 虛擬表實際上類似於一個指標陣列, 內部的 f 指向了真正的函式程式碼. 而虛擬表的另外一個位置, 內部存放了一些關於類別的信息, 以便於 typeid 等的運算子存取.

\blacksquare

既然虛擬函式可以出現在 constexpr 函式中, 那麼同樣儲存在虛擬表中的其它信息應該也可以被 constexpr 函式所接受. 自然地, C++ 20 提案 P1327R1《Allowing dynamic_cast, polymorphic typeid in Constant Expressions》提出了解除 dynamic_casttypeid 運算子在 constexpr 中的限制.

1.3.5 允許在 constexpr 函式中切換等位成員

由於 P0784R7 提出了解除記憶體配置在 constexpr 函式中的限制, 使得某些來自於 C++ 標準樣板程式庫中的容器可以進行編碼期計算 (特別是 std::string). 但是, 部分容器的實作借助了等位這一特性. 由於 C++ 20 之前, 在 constexpr 中不允許切換等位成員, 從而導致了即使大量解除了 constexpr 的限制, 借助等位實作的容器仍然無法是 constexpr 的. 因此, C++ 20 提案 P1330R0《Changing the active member of a union inside constexpr》提出了解除這個限制.

1.3.6 更改類別成員的預設初始化語義

我們知道, C 風格的結構體都是具有常數表達式性質的, 也就是它們都有在編碼期計算的能力. 例如

struct S {
    bool value;
};

然而, 類別 S 並不能直接在 constexpr 函式中初始化 :

struct S {
    bool value;
};

template <typename T>
constexpr T f(const T &other) {
    T s;        // Note : non-constexpr constructor 'S2' cannot be used in a constant expression
    s = other;
    return s;
}

int main(int argc, char *argv[]) {
    constexpr auto s {f(S {})};     // Error : constexpr variable 's' must be initialized by a constant expression
}

這個錯誤格外顯眼, 也格外獨特. 顯然, S 的建構子由編碼器生成並且類別 S 是有能力在編碼期進行計算的. C++ 20 提案 P1331R2《Permitting trivial default initialization in constexpr contexts》提出了解決這個問題, 使得上述程式碼在 C++ 20 中可以正常編碼.

1.3.7 允許 constexpr 函式中的匯編程式碼

在 C++ 17 中, constexpr 是不允許出現匯編程式碼的, 也就是帶有 constexpr 標識的函式中不能出現 asm, __asm 或者 __asm__ 這樣的關鍵字 (asm 是納入標準的關鍵字). C++ 20 提案 P1668R1《Enabling constexpr Intrinsics By Permitting Unevaluated inline-assembly in constexpr Functions》提出解除這一限制, 理由是函式內除了匯編程式碼部分, 其它部分很可能是 constexpr 友好的 :

template <typename T>
double f(T a, double b, double c) {
    if constexpr(std::is_same_v<T, double>) {
        return a * b + c;
    }
    asm(/* assembly code here */);
    return /* one of a, b or c */;
}

一旦函式 f 的樣板參數 T 被型別引數 double 所替換, 那麼函式 f 就有能力在編碼期完成計算, 也就不需要去運作 asm 中的匯編程式碼.

2. consteval

constexpr 函式雖然有能力在編碼期完成計算, 但並不總是這樣. 有時候, 如果我們想讓某個函式總是在編碼期計算, 則束手無策. 因此, C++ 20 提案 P1073R3《Immediate functions》提出了 consteval 關鍵字. 它的用法類似於 constexpr, 但是它只能用於函式. 帶有 consteval 標識的函式必定會在編碼期產生結果, 並且被稱為瞬時函式 (immediate function). 一旦編碼器檢測到當前帶有 consteval 標識的函式不是編碼期就能完成運作的時候, 便會擲出編碼錯誤.

consteval int f(int x, int n) {
    for(auto i {0}; i < n; ++i) {
        ++x;
        ++x;
    }
    return x + 42;
}
auto r {f(0, 10)};      // r 的值為 62, auto r {f(0, 10)} 等價於 auto r {62}.
constexpr auto r2 {f(0, 10)};
constexpr auto r3 {f(r, 10)};       // Error : call to consteval function 'f' is not a constant expression

Code 10 中, 儘管 r 的初始化值是一個常數表達式, 但是它並沒有被 constexpr 所標識, 因此自然不能應用到函式 f 中, 最終導致了 r3 初始化產生編碼錯誤.

瞬時函式是一種很特殊的函式. 我們可以使用函式指標 (或者參考) 指向一個帶有 cosntexpr 標識的函式, 甚至函式指標本身都可以是 constexpr 的. 然而, 我們不能使用函式指標或者函式參考指向一個帶有 consteval 標識的函式. 從某種程度上來說, 帶有 consteval 標識的函式相當於巨集.

需要注意的是, 解構子, 記憶體配置函式和回收函式不能被 consteval 標識. 另外, constevalconstexpr 標識不能同時出現.

一開始, 被 consteval 標識的函式是無法用於推導語境的 :

#include <type_traits>

template <typename T, typename U>
consteval auto add(const T &t, const U &u) noexcept(noexcept(t + u)) {
    return t + u;
}
using add_returning_type = decltype(add(std::declval<int &>(), std::declval<int>()));       // Error : call to consteval function 'add<int, int>' is not a constant expression

原因在於編碼器會錯誤地去判斷函式的呼叫是否為常數表達式, 然而上面這個呼叫不會有結果, 自然也就無法產生常數表達式, 於是編碼器擲出了編碼錯誤. 現在這個問題被 C++ 20 提案 P1937R2《Fixing inconsistencies between constexpr and consteval functions》修復, 使得在推導時, 保持 constexprconsteval 的一致性.

3. constinit

對於靜態變數的初始化問題, 一直是 C++ 的痛點之一. 不同檔案相互引入全域變數的時候, 需要注意來自於其它檔案的全域變數是否已經初始化, 否則很可能出現未定行為. 為了解決這個問題, 我們一般採用 static 函式 :

int &x() noexcept {
    static int x {};
    return x;
}
#include "a.hpp"

int y = x();

這種方式取代了原來使用 extern 宣告的方式, 保證了不會在初始化順序的問題下出現未定行為.

儘管 a.hpp 中的變數 x 被實作為了函式, 但是它看起來仍然帶有常數的屬性. 如果不是因為變數初始化不確定的問題, a.hpp 中的變數 x 就可以直接寫為 auto x {0}; 然後在 b.cpp 中引入之後, 直接使用即可.

上面這個問題在 constexpr 變數上並不存在, 因為它在編碼期就已經被初始化了. 然而, a.hpp 中的變數 x 不能帶有 constexpr 標識, 因為它要保持可變的屬性, 因此不能直接寫為 constexpr auto x {0}; 於是, x 就需要在運作期進行初始化, 然後初始化順序的問題便出現了.

為了解決這個問題, C++ 20 提案 P1143R2《Adding the constinit keyword》為 C++ 20 引入了 constinit 關鍵字. 若一個變數帶有 constinit 關鍵字, 就說明這個變數是靜態變數, 可以在編碼期對其進行初始化, 並且它是可改變的 (只要它不主動帶有 const 限定). 從而 a.hpp中的變數 x 的寫法可以直接改為

constinit auto x {0};

而不需要再擔心 b.cpp 中對 x 的使用會不會產生未定行為.

帶有 constinit 標識符的變數的儲存方式必定是靜態的或者 thread_local 的, 如果在運作期才對帶有 constinit標識的變數進行初始化, 會產生編碼錯誤.

根據 constexprconstinit 的定義, 我們可以推出, 帶有 constexpr 標識的變數實際上隱含了 constinit. 和 consteval 類似, constinitconstexpr 不能同時出現在宣告中.

4. std::is_constant_evaluated

C++ 20 還在標頭檔 <type_traits> 中引入了一個新的函式 std::is_constant_evaluated, 這個函式實際上我們已經在 《【C++】實作 <type_traits> (下)》中講述過了. 它的作用是判斷程式碼某個部分是否可以在編碼期進行計算. 如果某段程式碼可以在編碼期進行計算, 那麼編碼器會在編碼期直接運作這段程式碼, 而不是等到運作期 :

#include <cmath>
#include <type_traits>

constexpr double power(double x, unsigned y) {
    if(std::is_constant_evaluated() and y >= 0) {
        auto r {1.0}, p {x};
        while(y not_eq 0) {
            if(y & 1) {
                r *= p;
            }
            y /= 2;
            p *= p;
        }
        return r;
    }
    return std::pow(x, static_cast<double>(y));
}
constexpr auto k {power(10.0, 3)};      // OK
static_assert(k == 1000.0);     // OK
int n {3};
auto k2 {power(10.0, n)};       // OK, 呼叫 std::pow(10.0, 3.0);
constexpr k3 {power(10.0, n)};      // Error

那麼哪一些語境才會使得 std::is_constant_evaluated 回傳 true 呢? 為此, C++ 20 提案 P0595R2std::is_constant_evaluated()引入了一個全新的概念, 稱為顯著常數評估的 (manifestly constant-evaluated). 下面這些表達式 (包含轉型) 是顯著常數評估的表達式 :

  1. 常數表達式;
  2. if constexpr 陳述式中的判斷;
  3. 帶有 constexpr 標識的變數的初始化;
  4. 瞬時函式的呼叫;
  5. 制約表達式;
  6. 具有常數表達式屬性的變數初始化 (可以被用於編碼期計算的).

顯著常數評估的表達式必定是可以在編碼期可以計算的 (而不是潛在的). 對於處於顯著常數評估語境中的 std::is_constant_evaluated 必定會回傳 true, 否則會回傳 false.

有一個需要注意的地方是我們不能把 std::is_constant_evaluated 放入 if constexpr 中, 因為此時 std::is_constant_evaluated 總是回傳 true, 從而失去了它的意義.

不過有一個比較邊緣的問題 :

#include <type_traits>

constexpr int f() {
    const int n {std::is_constant_evaluated() ? 1 : 2};
    int m {std::is_constant_evaluated() ? 1 : 2};
    return n + m;
}
auto r1 {f()};      // r1 = ???
constexpr auto r2 {f()};        // r2 = ???

Code 15 中, r1r2 的值分別為多少? 很顯然, 對於函式 f 中的變數 n, 其值符合顯著常數評估的表達式中的條件六, 從而不論在編碼期還是運作期, n 的值都是 1. 然而對於 m, 情況就不太相同了. 不過我們知道, 在運作期, m的值一定是 2, 因此 r1 的值一定是 3. 因為 r1 的初始化是在運作期, 而不是在編碼期. 那麼現在的問題就是有沒有可能在編碼期就可以計算 m 的值. 由於函式 f 帶有 cosntexpr 標識, 它是有可能在編碼期進行計算的. 同樣根據顯著常數評估的表達式中的條件六, 當函式 f 在編碼期進行計算的時候, m 的值就是 1. 故 r2 的值為 2.

一個不帶有參數的函式, 在不修改程式碼的情況下產生了兩個不同的值, 確實比較神奇.

5. if consteval

if consteval 是 C++ 2b 引入的. 根據目前 C++ 標準的更新速度來說, 它很可能納入 C++ 23 標準.

在引入 constevalstd::is_constant_evaluated 之後, 我們便可以寫出如下程式碼 :

consteval int f(int i) {
    return i;
}
constexpr int g(int i) noexcept {
    if(std::is_constant_evaluated()) {
        return f(i) + 1;        // Error : call to consteval function 'f' is not a constant expression
    }else {
        return 42;
    }
}
consteval int h(int i) {
    return f(i) + 1;
}

設計非常完美, 我們提供了兩個關於 f 的函式 : gh. 當編碼期計算可行的時候, 我們就會直接使用函式 h; 當存在運作期才能計算的可能時, 我們使用函式 g. 但是很遺憾, 函式 g 出現了編碼錯誤, 原因是 f(i) 的呼叫不被編碼期評定為常數表達式, 即使 std::is_constant_evaluated 回傳了 true. 原因就在於 f(i) 所處的語境是 constexpr 函式, 而非 consteval 函式, 因此對 f(i) 的限制沒有 consteval 嚴格. 換句話說, 從 constexpr 函式內有條件地去呼叫 consteval 函式不可行.

std::is_constant_evaluated 還有一個問題, 在之前我們已經提到過, 就是將其放入 if constexpr 中. 我們並沒有說這個是錯誤的, 而是說這個行為是無意義的. 截至發文為止, Clang 和 MSVC 都對此擲出了編碼警告, 但是 GCC 並沒有. 因此, 在編碼期跟進之前, 我們並不能完全指望編碼期幫助我們尋找這種隱匿的錯誤, 就像我們不小心將 if(x == y) 寫為了 if(x = y) 類似.

綜合上面的問題, C++ 2b (expected C++ 23) 提案 P1938R3if consteval提出了 if consteval. 它的作用類似於 std::is_constant_evaluated, 但是用法不太一樣 :

consteval int f(int i) {
    return i;
}
constexpr int g(int i) noexcept {
    if consteval {
        return f(i) + 1;        // OK
    }else {
        return 42;
    }
}
consteval int h(int i) {
    return f(i) + 1;
}

不同於 if constexpr, if consteval 不帶有任何條件, 並且它後面的大括號是強制性的, 不能省略. 當 f(i) + 1 可以在編碼期進行計算的時候, 就採用該分支; 否則, 就採用 else 分支 (若有). 除此之外, 我們使用 std::is_constant_evaluated 的時候, 必須匯入標頭檔 <type_traits>, 而使用 if consteval 就不需要. 寫法上也和 std::is_constant_evaluated 不同, 從而避免了將 std::is_constant_evaluated 錯用在 if constexpr 中的情況.

由於 if consteval 不能帶有任何條件, 因此相反的情況我們使用 if not consteval 或者 if !consteval 來表示. 而條件可以在下一層使用 if 或者 if constexpr 寫出 :

constexpr void f() {
    if consteval {
        if(/* ... */) {
            // ...
        }else {
            // ...
        }
    }else {
        // ...
    }
}

P1938R3 提出這種設計是為了避免未來程式碼中出現 if constexprif consteval 導致程式碼混亂, 破壞 C++ 程式碼的優雅性.

Tip : 作為一個喜歡寫程式庫的人來說, if constexpr 確實有用, 但是我認為它同時破壞了 C++ 程式碼的優雅性. 上面這樣的設計同樣也破壞了 C++ 程式碼的優雅性.

至於 if consteval 為什麼一定要附帶大括號, P1938R3 給出了一個理由, 就是如果未來支援了 if consteval for, if consteval while, if consteval switch, 那麼 if consteval for(auto i {0}; i < n; ++i) 是表示

if consteval for(auto i {0}; i < n; ++i) {
    // ...
}

呢? 還是

if consteval {
    for(auto i {0}; i < n; ++i) {
        // ...
    }
}

呢? 這裡會存在一些容易混淆的情況. 因此, P1938R3 提出了 if consteval 不能省略大括號的要求.

最後, P1938R3 提出了遺棄 std::is_constant_evaluated, 因為 if constavel 的提出使得 std::is_constant_evaluated 的存在沒有意義, 而且通過 if consteval 可以實作 std::is_constant_evaluated :

constexpr bool is_constant_evaluated() noexcept {
    if consteval {
        return true;
    }
    return false;
}