constexpr 的來龍去脈

constexpr 是什麼?

關鍵字 constexpr (constant expression) 是在 C++11 中引入的,並且在 C++14 中進行了優化。

constexpr 和 const 一樣可以用來修飾變數:試圖修改 constexpr 變數時,編譯器將會報錯。

不同於 const, constexpr 還可以修飾函數和類的構造函數。 constexpr 表示值或者返回值是常量,並且如果可能,在編譯時計算它們。

一個 constexpr 整型值能夠用在任何 const 整型值可以用的地方,例如模板參數和數組的申明。

當值在編譯時計算而不是運行時計算時,它能夠使程式運行得更快,並使用更少的記憶體。

為了限制編譯時常量計算的複雜性,以及其對編譯時間潛在的影響, C++14 標準需要 constexpr 類型必須為字面值類型。

語法

constexpr literal-type identifier = constant-expression ;
constexpr literal-type identifier { constant-expression } ;
constexpr literal-type identifier ( params ) ;
constexpr ctor ( params ) ;

參數

params
一個或者多個參數, 每個參數必須是字面值類型並且本身是 常量表達式 。

返回值

一個 constexpr 變數或者函數必須返回字面值類型。

constexpr 變數

constexpr 和 const 的主要區別是 const 變數的初始化可以被延時到運行時,而 constexpr 變數必須在編譯時初始化。所有的 constexpr 變數都是 const。

  • 當變數由字面值類型初始化時,能夠聲明為 constexpr。如果初始化時由構造函數執行的,那麼該構造函數也必須聲明為 constexpr。

  • 引用類型可以聲明為 constexpr 只要滿足:引用對象是由 常量表達式初始化的,並且初始化時任何隱式轉換也是常量表達式。

  • 所有 constexpr 變數或者函數的聲明必須擁有 constexpr 說明符。

舉例:

constexpr float x = 42.0;
constexpr float y{108};
constexpr float z = exp(5, 3);
constexpr int i; // Error! Not initialized
int j = 0;
constexpr int k = j + 1; //Error! j not a constant expression

constexpr 函數

constexpr 函數的返回值是在編譯時計算的。調用程式碼需要返回值在編譯時初始化一個 constexpr 變數, 或者提供一個非類型模板參數。 當參數是 constexpr 值時,constexpr 函數產生一個編譯時的常量。當被調用時傳入非 constexpr 參數,或者其返回值在非編譯時請求,constexpr 函數和普通函數一樣將產生提個運行時的值。(這種行為能夠讓你避免編寫兩個相同功能的函數,一個為 constexpr 版本,一個為非 constexpr 版本)

constexpr 函數或者構造函數默認是 inline 的.

以下規則適用於 constexpr 函數:

  • 必須接受並返回字面值類型。
  • 可以是遞歸的.
  • 不能是虛構的。當類有虛基類時,構造函數不能定義為 constexpr。
  • 函數體可以定義為 = default 或者 = delete。
  • 函數體不能包含 goto 語句或者 try 塊。
  • 顯式特化/具體化(explicit specialization)的非 constexpr 模板能夠聲明為 constexpr。
  • 顯式特化/具體化(explicit specialization)的非 constexpr 模板不必聲明為 constexpr。

以下 constexpr 函數規則適用於 Visual Studio 2017 及以後的版本:

  • 可以包含 if 和 switch 語句,以及所有循環語句包括 for,while 和 do-while
  • 可以包含初始化的局部變數,並且必須是字面值類型,不能是 static 或者 thread-local。該局部變數不必是 const 的。
  • 非 static 的 constexpr 成員函數不必隱式為 const
constexpr float exp(float x, int n)
{
    return n == 0 ? 1 :
        n % 2 == 0 ? exp(x * x, n / 2) :
        exp(x * x, (n - 1) / 2) * x;
}

提示:

在 Visual Studio 調試器中, 當調試一個非優化的調試版本,你能夠通過在函數內部設置斷點來區分 constexpr 函數是否在編譯時計算。如果斷點能夠觸發,則為運行時計算,否則,為編譯時計算。

示例

以下例子演示了 constexpr 變數, 函數, 以及用戶自定義類型。 在 main() 最後, constexpr 成員函數 GetValue() 是在運行時調用的,因為其返回值沒有被要求在編譯時確定。

#include <iostream>

using namespace std;

// Pass by value
constexpr float exp(float x, int n)
{
    return n == 0 ? 1 :
        n % 2 == 0 ? exp(x * x, n / 2) :
        exp(x * x, (n - 1) / 2) * x;
}

// Pass by reference
constexpr float exp2(const float& x, const int& n)
{
    return n == 0 ? 1 :
        n % 2 == 0 ? exp2(x * x, n / 2) :
        exp2(x * x, (n - 1) / 2) * x;
}

// Compile-time computation of array length
template<typename T, int N>
constexpr int length(const T(&)[N])
{
    return N;
}

// Recursive constexpr function
constexpr int fac(int n)
{
    return n == 1 ? 1 : n * fac(n - 1);
}

// User-defined type
class Foo
{
public:
    constexpr explicit Foo(int i) : _i(i) {}
    constexpr int GetValue() const
    {
        return _i;
    }
private:
    int _i;
};

int main()
{
    // foo is const:
    constexpr Foo foo(5);
    // foo = Foo(6); //Error!

    // Compile time:
    constexpr float x = exp(5, 3);
    constexpr float y { exp(2, 5) };
    constexpr int val = foo.GetValue();
    constexpr int f5 = fac(5);
    const int nums[] { 1, 2, 3, 4 };
    const int nums2[length(nums) * 2] { 1, 2, 3, 4, 5, 6, 7, 8 };

    // Run time:
    cout << "The value of foo is " << foo.GetValue() << endl;
}

constexpr 和 const 的異同

相同處:

constexpr 和 const 一樣可以用來修飾變數:試圖修改 constexpr 變數時,編譯器將會報警。一個 constexpr 整型值能夠用在任何 const 整型值可以用的地方,例如模板參數和數組的申明。

不同處:

constexpr 還可以修飾函數和類的構造函數。
constexpr** 表示值或者返回值是常量,並且如果可能,在編譯時計算它們。
當一個值在編譯時計算而不是運行時計算時,它能夠使程式運行得更快,並使用更少的記憶體。

所有的 constexpr 對象都是 const 的,但不是所有的 const 對象都是 constexpr 的。

示例

int sz;                             // non-constexpr variable
…

constexpr auto arraySize1 = sz;     // error! sz's value not
                                    // known at compilation

std::array<int, sz> data1;          // error! same problem

constexpr auto arraySize2 = 10;     // fine, 10 is a 
                                    //compile-time constant

std::array<int, arraySize2> data2;  // fine, arraySize2
                                    // is constexpr
int sz;                             // as before
…

const auto arraySize = sz;          // fine, arraySize is
                                    // const copy of sz

std::array<int, arraySize> data;    // error! arraySize's value
                                    // not known at compilation

應該使用 constexpr 的場景

只要允許,儘可能使用 constexpr,當值在編譯時計算而不是運行時計算時,它能夠使程式運行得更快,並使用更少的記憶體。

不應該使用 constexpr 的場景

constexpr 是對象或者函數介面的一部分,所以如果你使用了 constexpr 但反悔了,移除 constexpr 可能會導致大量的調用程式碼編譯失敗。(比如添加 I/O 操作用於調試或者性能調優可能導致這樣的問題,因為 I/O 語句通常不是在 constexpr 函數中執行的。)

引用

Tags: