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,而是說:若 base 與 exp 是編譯期常量,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 中 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 嗎」 | 不保證;也不保證編譯期已知——這是特性非缺陷 |
Related Notes
- 04-Moving-To-Modern-Cpp/08-Noexcept — 本 Item 範例中 constexpr 函式多同時宣告 noexcept;兩者皆是「介面的一部分」的長期承諾
- 04-Moving-To-Modern-Cpp/10-Const-Member-Functions-Thread-Safety — C++11 constexpr 成員函式隱含 const;const 成員函式的進一步議題(執行緒安全)
- 04-Moving-To-Modern-Cpp/04-Scoped-Enums — enumerator 值是要求整數常量運算式的典型語境
- 03-Auto/01-Prefer-Auto-To-Explicit-Types —
constexpr auto宣告結合型別推導與編譯期常量 - 04-Moving-To-Modern-Cpp/Practice-Moving-To-Modern-Cpp — 本章練習題