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