C++ 面試題:C++中 constexpr 函數的限制有哪些?
注意這道面試題,問的不是 constexpr 的用法,是限制有哪些?
一、基本限制
參數和返回類型必須是字面類型。
我們理解下什么是字面類型?
在 C++ 中,字面類型(Literal Type) 是指可以在編譯期確定其值的類型,是支持編譯期計算的基礎。
1. C++ 標準規定,以下類型屬于字面類型:
(1) 基本類型
int, char, bool, float, double, long, short, unsigned 等。
nullptr_t(C++11 起)。
constexpr int x = 42; // int 是字面類型
constexpr char c = 'A'; // char 是字面類型
(2) 引用類型
引用必須綁定到字面類型。
constexpr int a = 10;
constexpr const int& ref = a; // 引用是字面類型
(3) 數組類型
數組的元素必須是字面類型。
constexpr int arr[] = {1, 2, 3}; // int[] 是字面類型
(4) 字面值類(Literal Class)
類的所有非靜態成員必須是字面類型。
必須有一個 constexpr 構造函數(可以是默認構造函數或帶參數的構造函數)。
不能有虛函數。(C++20允許字面類型包含虛函數,但是需要滿足不少條件)
struct Point { // 字面值類
int x, y;
constexpr Point(int x = 0, int y = 0) : x(x), y(y) {} // constexpr 構造函數
};
constexpr Point p(1, 2); // 編譯期構造
(5) void(C++14 起)
void 也可以算作字面類型,但通常不能直接用于 constexpr 變量。
(6) 標準庫中的某些類型
std::array(如果 T 是字面類型)。
std::string_view(C++17 起)。
#include <array>
constexpr std::array<int, 3> arr = {1, 2, 3}; // std::array 是字面類型
constexpr std::string_view sv = "compile-time"; // 合法,數據是編譯期字面量
// constexpr std::string_view sv2 = std::string("runtime"); // 錯誤:非編譯期數據
std::string("runtime") 會創建一個臨時 std::string 對象,它的底層數據(存儲字符的數組)在內存中的生命周期僅限于當前表達式。當這行代碼執行完畢時,臨時對象會被銷毀,其底層數據也隨之失效。
std::string("runtime") 生成的臨時對象在編譯期上下文中仍然會“邏輯銷毀”,導致 string_view 引用的底層數據在編譯期就失效。
這里最關鍵的就是數據來源的編譯期確定性!
2. 非字面類型的例子
以下類型不是字面類型,因此不能用于 constexpr 上下文:
- std::string(因為它的動態內存分配不能在編譯期確定)。
- 帶有虛函數的類(C++20 之前)。
- 包含非字面類型成員的類。
struct NonLiteral {
std::string s; // std::string 不是字面類型
NonLiteral() {} // 沒有 constexpr 構造函數
};
// constexpr NonLiteral nl; // 錯誤:NonLiteral 不是字面類型
3. 為什么 constexpr 限制要求字面類型?
constexpr 的核心目標是編譯期計算,因此:
- 編譯期可構造:字面類型的對象可以在編譯期初始化。
- 編譯期可求值:constexpr 函數的參數和返回值必須是編譯期可確定的。
- 避免運行時依賴:非字面類型(如 std::string)可能涉及動態內存分配,無法在編譯期處理。
二、禁止的操作
以下操作在 constexpr 函數中不允許出現:
1. 動態內存分配
用new/delete 或堆內存操作。
constexpr int* invalid() {
int* p = new int(42); // 錯誤:不能在編譯時分配內存
return p;
}
2. 異常處理
不能使用 throw 或 try-catch。
constexpr int unsafe(int a) {
if (a < 0) throw "negative"; // 錯誤:不允許異常
return a;
}
3. 調用非 constexpr 函數
只能調用其他 constexpr 函數或編譯器內建函數
int non_constexpr(int x) { return x; }
constexpr int invalid_call(int x) {
return non_constexpr(x); // 錯誤:調用了非 constexpr 函數
}
4. 修改全局/靜態變量
編譯時上下文無法處理副作用。
全局變量:在程序啟動時(main() 之前)初始化。
靜態變量:
- 局部 static 變量在第一次進入作用域時初始化(運行時)。
- 全局 static 變量類似于全局變量。
由于它們的初始化可能依賴運行時狀態,constexpr 無法保證編譯期確定性。
int global = 0;
constexpr void modify_global() {
global++; // 錯誤:修改全局變量
}
C++標準規定,constexpr函數中不能包含對具有靜態存儲期變量的賦值或修改操作。
三、成員函數的特殊規則
1. 虛函數
- C++20 前:虛函數不能是 constexpr。
- C++20 起:允許虛函數為 constexpr。
struct Base {
virtual constexpr int foo() { return 1; } // C++20 合法
};
2. 隱式 const 限定(C++11)
- C++11:constexpr 成員函數隱式為 const。
- C++14:取消此限制,允許修改對象狀態。
struct Widget {
int value = 0;
constexpr void update() { value++; } // C++14+ 合法
};
這里其實開始不是很理解,成員變量的修改其實是運行時行為,但是現在要在編譯期搞,查了下資料是這么說的:
constexpr成員函數修改成員變量,在編譯期是邏輯行為,運行時才是真實修改
。是邏輯上的(編譯器模擬,不生成實際的內存寫入)。
我理解是:
- 對 value 的修改發生在編譯期,最終生成的 value 是一個編譯期常量對象,其狀態被“凍結”為 count = 1。
- 沒有運行時開銷,value 的值直接編譯進二進制。
- 這里的“修改”只是邏輯上的操作,不涉及真實內存寫入。
- 運行時調用 update() 是真正的運行時行為,修改的是內存中的對象。
- 代碼邏輯與編譯期版本相同,但發生在程序運行時。
- 對比上面說的全局變量,類成員變量的對象是局部的影響可控,全局變量可能被其他地方修改,所以類成員變量這里可以放開,但是全局變量不行。
四、遞歸深度限制
即使遞歸邏輯合法,編譯器對 constexpr 遞歸深度有默認限制(如 GCC 默認 512 層)。超出限制時需通過編譯選項調整:
g++ -fconstexpr-depth=1000 main.cpp
五、版本差異總結
特性 | C++11 | C++14+ | C++20 |
函數體復雜度 | 單條 | 允許循環、變量 | 進一步擴展 |
虛函數支持 | 不支持 | 不支持 | 支持 |
成員函數隱式 | 是 | 否 | 否 |
示例:合法與非法用法對比
// 合法:C++14+ 允許循環和局部變量
constexprintsum(int n){
int total = 0;
for (int i = 0; i < n; ++i) {
total += i;
}
return total;
}
// 非法:動態內存分配
constexprint* create(){
int* p = newint(10);
return p;
}
// 合法:C++20 虛函數
structBase {
virtualconstexprintget(){ return1; }
};