Item 15:盡可能使用 constexpr (Use constexpr whenever possible)

Overview Table

面向 重點
constexpr 物件 必為 const,且以編譯期已知的值初始化
constexpr 函式 傳入編譯期常量 → 產生編譯期結果;傳入執行期值 → 如普通函式在執行期計算
與 const 的差異 const 不保證編譯期已知;需要 compile-time constant 時只能靠 constexpr
適用範圍 陣列大小、整數 template 引數(如 std::array 長度)、enumerator 值、alignment 等要求 integral constant expression 的語境
核心建議 constexpr 物件與函式可用的語境比 non-constexpr 更廣 → 盡可能使用
介面承諾 constexpr 是物件/函式介面的一部分,事後移除會破壞大量 client code
最容易混淆的一點

不能假設 constexpr 函式的結果是 const,也不能假設其值在編譯期已知——這是刻意設計的特性(同一函式同時服務編譯期與執行期),不是缺陷。

constexpr 物件:加強版的 const

constexpr 物件必為 const,且值在編譯期確定,因此可放入唯讀記憶體(對嵌入式開發尤其重要),並可用於所有要求整數常量運算式的語境。

int sz;                              // 非 constexpr 變數(執行期值)

constexpr auto arraySize1 = sz;      // 錯誤!sz 的值編譯期未知
std::array<int, sz> data1;           // 錯誤!同樣問題

constexpr auto arraySize2 = 10;      // OK,10 是編譯期常量
std::array<int, arraySize2> data2;   // OK,arraySize2 是 constexpr

const auto arraySize = sz;           // OK,const 只是 sz 的唯讀副本
std::array<int, arraySize> data;     // 錯誤!const 不保證編譯期已知
比較 const constexpr
唯讀(不可修改)
保證編譯期已知的值 (可用執行期值初始化)
可當陣列大小 / template 引數 不一定 一定可以

記法:所有 constexpr 物件都是 const,但並非所有 const 物件都是 constexpr。

「編譯期」的精確說法

技術上 constexpr 物件的值是在 translation(編譯 + 連結)期間決定的;除非你在開發編譯器或連結器,實務上可直接視為「編譯期已知」。

constexpr 函式:一份程式碼、兩種模式

constexpr 函式以引數是否為編譯期常量決定行為,一個函式同時取代「編譯期版」與「執行期版」兩份程式碼。

              呼叫 constexpr 函式
                      │
        ┌─────────────┴─────────────┐
   所有引數編譯期已知?              有引數僅執行期可知?
        │                           │
   結果可在編譯期計算            如普通函式,執行期計算
        │                           │
  可用於 std::array 大小、      (若此時用在要求編譯期
  template 引數、enum 值等       常量的語境 → 編譯錯誤)
constexpr int pow(int base, int exp) noexcept   // C++14 實作
{
  auto result = 1;
  for (int i = 0; i < exp; ++i) result *= base; // C++14 允許迴圈
  return result;
}

constexpr auto numConds = 5;                 // 實驗條件數
std::array<int, pow(3, numConds)> results;   // 編譯期算出 3^5 個元素

auto base = readFromDB("base");              // 執行期取得
auto exp  = readFromDB("exponent");
auto baseToExp = pow(base, exp);             // 同一函式,執行期計算

pow 前的 constexpr 不是說回傳值是 const,而是說:若 baseexp 是編譯期常量,pow 的結果可以當編譯期常量使用。

限制 C++11 C++14
函式本體 至多一條 return 述句(用 ?: 代替 if-else、遞迴代替迴圈) 大幅放寬,可用區域變數、迴圈、if 等
參數/回傳型別 必須是 literal type;void 不算 void 也可(setter 可為 constexpr)
constexpr 成員函式 隱含 const(不能修改 *this 不再隱含 const

使用者定義型別也能是 literal type

建構子與成員函式皆可宣告 constexpr,讓自訂類別參與編譯期計算。

class Point {
public:
  constexpr Point(double xVal = 0, double yVal = 0) noexcept
  : x(xVal), y(yVal) {}

  constexpr double xValue() const noexcept { return x; }
  constexpr double yValue() const noexcept { return y; }

  constexpr void setX(double newX) noexcept { x = newX; }  // 僅 C++14
  constexpr void setY(double newY) noexcept { y = newY; }  // 僅 C++14

private:
  double x, y;
};

constexpr Point p1(9.4, 27.7);      // 編譯期「執行」constexpr 建構子
constexpr Point p2(28.8, 5.3);

constexpr Point midpoint(const Point& p1, const Point& p2) noexcept
{
  return { (p1.xValue() + p2.xValue()) / 2,   // 呼叫 constexpr getter
           (p1.yValue() + p2.yValue()) / 2 };
}

constexpr auto mid = midpoint(p1, p2);   // mid 可建立於唯讀記憶體!

constexpr Point reflection(const Point& p) noexcept   // C++14
{
  Point result;                 // 區域非 const Point
  result.setX(-p.xValue());     // constexpr setter
  result.setY(-p.yValue());
  return result;
}

傳統上「編譯期工作 vs 執行期工作」的界線因此變得模糊:越多計算移往編譯期,執行期越快(但編譯可能變慢)。

浮點數的限制

mid.xValue() * 10 型別是 double;浮點型別不能直接用來實例化 template 或指定 enumerator 值,須先如 static_cast<int>(mid.xValue() * 10) 轉成整數型別,再用於這些語境。

C++11 的 setter 限制

C++11 中 setX/setY 不能是 constexpr:(1) constexpr 成員函式隱含 const,不得修改物件;(2) 回傳型別 void 在 C++11 不是 literal type。兩項限制在 C++14 皆已解除。

constexpr 是介面的一部分

宣告 constexpr 等於向 client 承諾:「我可以用在要求常量運算式的語境」。一旦 client 依賴此承諾,事後移除 constexpr 會使任意大量的 client code 無法編譯

常見翻車情境

只是為了除錯在 constexpr 函式內加一行 I/O(如 printf),就可能使其失去 constexpr 資格,連帶炸掉所有把它用在編譯期語境的 client code——I/O 述句一般不允許出現在 constexpr 函式中。

「盡可能使用(whenever possible)」的真義:包含你願意長期承擔 constexpr 所施加的實作限制。

Exam/Test Patterns

情境關鍵字 答案
「const 變數當 std::array 大小」 不一定可行;const 不保證編譯期已知,需要 constexpr
「constexpr 物件是不是 const?」 ;反之 const 物件不一定是 constexpr
「constexpr 函式傳入執行期引數」 合法,退化為普通函式在執行期計算
「constexpr 函式用於陣列大小但引數執行期才知道」 編譯錯誤(該語境要求編譯期常量)
「C++11 constexpr 函式本體限制」 單一 return;用 ?:遞迴繞過
「C++14 constexpr 有何放寬」 允許迴圈/區域變數、void 回傳、成員函式不再隱含 const(setter 可 constexpr)
「constexpr 建構子的意義」 使自訂型別成為 literal type,物件可於編譯期建構、放入唯讀記憶體
「移除函式的 constexpr 會怎樣」 破壞 client code——constexpr 是介面的一部分,屬長期承諾
「constexpr 函式回傳值是 const 嗎」 不保證;也不保證編譯期已知——這是特性非缺陷