上一篇文的低延遲系統的大哉問中,提到了幾個在 compile time 常見且實用的語法。接下來我們將進一步拆解這些語法的運作方式,了解它們各自適合的使用情境,以及如何在實際開發中發揮作用。
1. template —— 不只是 Generic Programming
template 除了能讓同一份 code 支援不同的 type,更重要的是把原本需要 runtime 決定的資訊提前到 compile time。這也是為什麼 C++ template 常常被視為一種 compile-time programming 而不只是 generic programming。
template <OrderProcessor T>
void process_order(T& processor, const Order& order) {
processor.process(order);
}
需注意若有大量 template instantiation,可能會產生很多份不同的 machine code,造成以下問題:
2. constexpr —— 不一定在 Compile Time 執行
很多人看到 constexpr 關鍵字就認為 constexpr function 一定在 compile time 執行,其實不然。
constexpr int square(int x) {
return x * x;
}
int x = 10;
int y = square(x);
上述例子中的 x 是 runtime value,所以 square(x) 仍然可以在 runtime 執行。constexpr 更精確的意思是這個 function 被設計成可以在 constant evaluation 中執行。
如果 input 在 compile time 已知,那麼 compiler 就可以在 compile time 算出 y == 100:
constexpr int x = 10;
constexpr int y = square(x);
constexpr 允許 compile-time evaluation,而不是強制 compile-time execution。
3. consteval —— 一定在 Compile Time 執行
如果我們真的希望某個 function 必須在 compile time 執行,可以使用 consteval。
consteval int square(int x) {
return x * x;
}
constexpr int x = square(10); // ✅
int x;
std::cin >> x;
int y = square(x); // ❌
因為 x 是 runtime value,而 consteval function 必須在 compile time evaluation 中完成,這使得 constexpr 與 consteval在語意上有明確差異,因而導致程式報錯。
在 low-latency programming 裡,consteval 可以把「這個 cost 不應該出現在 runtime」變成 API contract。
4. concept —— 讓 Static Polymorphism 更容易維護
如果過度使用 template、constexpr、inline,反而會降低程式的可讀性。C++20 的 concept 則可以在維持 static polymorphism 的情況下讓 interface 更清楚:
template <typename T>
concept OrderProcessor =
requires(T processor, const Order& order) {
processor.process(order);
};
Runtime polymorphism 的流程如下:
interface → virtual dispatch → implementation
Static polymorphism 則有以下差異:
concept → compile-time validation→ concrete implementation → inline / optimization
concepts 讓我們可以建立 compile-time interface,把原本 runtime object hierarchy 的部分設計轉移到 compile time。
5. if constexpr —— 把 Branch 移到 Compile Time
另一個很實用的工具是 if constexpr:
template <typename T>
void process(T& processor) {
if constexpr (requires { processor.fast_path(); }) {
processor.fast_path();
} else {
processor.normal_path();
}
}
這裡的判斷不是 runtime branch,compiler 在 compile time 就知道 T 是否有 fast_path。因此最後產生的 machine code 可以只保留需要的 branch,這和 if-else statement 有本質上的不同。
普通的 if:
runtime → evaluate condition → branch
而 if constexpr在某些情況下能讓 hot path 更簡單:
compile time → choose implementation → generate only required code
6. Compile-Time Computation 小總結
Compiler 可以直接把結果當成 constant 使用。std::array<Order, entries> orders; 中的 entries 是 compile-time constant,因此 array 的 size 在 compile time 就已經確定。這和 std::vector<Order> orders(n); 有很不同的 runtime semantics。
std::vector 的 size 是 runtime information,通常需要處理 dynamic allocation、capacity、initialization,以及可能的 growth;而固定大小的 std::array 則能讓 size 直接成為 compile-time information,減少 runtime 上需要處理的動態行為。因此,在 latency-sensitive system 中,如果資料的 size 本身就是已知的,使用 std::array<T, N> 往往比 std::vector<T> 更合理。
Compile-time computation 也常被用來建立 lookup table,將固定的計算結果預先產生,讓 runtime 只需要 lookup。不過 table 太大時可能增加 cache、TLB 與 binary size 的壓力,因此是否更快仍需根據實際 workload 衡量。