C++ 17 將 Lambda Expression 納入常數表達式(constexpr
)的範疇,所以我們也能在只接受編譯期常數的地方呼叫 Lambda Expression 定義的 Lambda Function:
void test() {
auto f = [](int a, int b) constexpr {
return a + b;
};
static_assert(f(1, 2) == 3, "");
}
本文會先回顧 C++ 11 與 C++ 14 標準定義的 constexpr
再介紹 C++ 17 的改進。
C++ 11 constexpr
C++ 11 首次引入常數表達式的概念,讓我們能以 constexpr
定義編譯期常數:
constexpr int size = 5;
int main() {
static_assert(size > 0, "");
int example[size] = {0};
}
同時,我們也能以 constexpr
定義「能於編譯期求值」的 Constexpr Function:
#include <iostream>
constexpr int fib(int n) {
return (n < 2) ? n : fib(n - 1) + fib(n - 2); // #1
}
int main() {
static_assert(fib(6) == 8, "");
int input;
if (std::cin >> input) {
std::cout << fib(input) << std::endl; // #2
}
}
上面的程式碼有兩個重點:
- 在 C++ 11 標準下,Constexpr Function 只能有一個 Return 述句。
- Constexpr Function 的實際參數可以是編譯期常數以外的數值。如果實際參數不是編譯期常數,該函式呼叫的求值(Evaluate)過程會延後至執行期(Run-time)。這個設計是讓「編譯期常數」與「一般數值」能共用同一份函式實作。
除此之外,我們也能以 constexpr
修飾建構式(Constructor)。例如:以下程式碼分別以 constexpr
修飾建構式與成員函式,接著在常數表達式當中建構 Rect
物件並呼叫 get_area()
成員函式(求值過程都於編譯期完成):
class Rect {
private:
int height_;
int width_;
public:
constexpr Rect(int height, int width)
: height_(height), width_(width) {}
constexpr int get_area() const {
return height_ * width_;
}
};
int main() {
static_assert(Rect(3, 5).get_area() == 15, "");
}
C++ 14 constexpr
C++ 14 稍微放寬 Constexpr Function 的限制。Constexpr Function 能使用更多種宣告或述句。例如前面的 fib
函式能以 for
述句改寫:
#include <iostream>
constexpr int fib(int n) {
int a = 0;
int b = 1;
for (int i = 0; i < n; ++i) {
int c = a + b;
a = b;
b = c;
}
return a;
}
int main() {
static_assert(fib(6) == 8, "");
int runtime_input;
if (std::cin >> runtime_input) {
std::cout << fib(runtime_input) << std::endl;
}
}
然而因為 C++ 14 標準的制定時程,C++ 14 來不及將 Lambda Expression 納入常數表達式。呼叫 Lambda Function 的表達式也不被視為常數表達式:
constexpr int test_lambda() {
auto f = []() { // compile-time error
return 3;
};
return f(); // compile-time error
}
作為替代方案,我們能以 Function Object 模擬 Lambda Function:
constexpr int test_func_obj() {
class F {
public:
constexpr int operator()() {
return 3;
};
};
return F()();
};
C++ 17 constexpr 與 Lambda Expression
C++ 17 再次擴張常數表達式的涵蓋範圍,讓我們能在常數表達式裡面定義或呼叫 Lambda Function。
Lambda Expression 求值過程
為了能準確地講解 C++ 17 的改動,讓我先離題講解 Lambda Expression 的求值過程。編譯器在處理 Lambda Expression 時,會合成一個帶有 operator()
成員函式的 Closure Type(閉包型別)。被 Lambda Expression 捕捉的區域變數也會成為該 Closure Type 的成員變數。接著,Lambda Expression 會被代換為「以區域變數建構 Closure Object(閉包物件)」的代碼。以下面的函式為例:
#include <functional>
std::function<void (int)> test(int a, int b) {
return [a, b](int x) {
return a * x + b;
};
}
概念上,編譯器會將上面的程式碼轉化為:
#include <functional>
class _ZZ4testiiENKUliE_ {
private:
int a;
int b;
public:
_ZZ4testiiENKUliE_(int a_, int b_) : a(a_), b(b_) {}
int operator()(int x) const {
return a * x + b;
}
};
std::function<void (int)> test(int a, int b) {
return _ZZ4testiiENKUliE_(a, b);
}
另外,我們有時也會將 Closure Object 儲存於區域變數內:
std::function<void (int)> test(int a, int b) {
auto f = [](int x) {
return a * x + b;
};
return f;
}
這就相當於:
class _ZZ4testiiENKUliE_ { /* ... */ };
std::function<void (int)> test(int a, int b) {
auto f = _ZZ4testiiENKUliE_(a, b);
return f;
}
宣告 Constexpr Lambda Expression
為了能在常數表達式裡面定義或呼叫 Lambda Function,C++ 17 有以下改動:
- 如果 Lambda Expression 捕捉的區域變數是 Literal Type(常數表達式能使用的型別),則與 Lambda Expression 對應的 Closure Type 也會被視為 Literal Type。
- 新增 Constexpr Lambda Expression 的語法。
- 如果一個 Lambda Expression 滿足 Constexpr Function 的要求,即使沒有明確宣告,也能被編譯器推定為 Constexpr Lambda Expression。
首先,要明確地將一個 Lambda Expression 宣告為 Cosntexpr Lambda Expression,只需要在參數列表之後、回傳型別之前加上 constexpr
關鍵字(如果省略回傳型別,則要在大刮號之前加上):
void test() {
auto fib = [](int n) constexpr -> int { // #1
int a = 0, b = 1;
for (int i = 0; i < n; ++i) {
int tmp = a + b;
a = b;
b = tmp;
}
return a;
};
static_assert(fib(5) == 5, "");
static_assert(fib(6) == 8, "");
}
Constexpr Lambda Expression 和 Constexpr Function 一樣必需滿足以下規定[1]:
- 回傳型別是 Literal Type
- 參數型別都是 Literal Type
- 函式定義:
- 沒有使用內嵌組合語言(Inline Assembly)
- 沒有使用
goto
述句或 Label - 沒有使用
try ... catch ...
述句(C++ 20 會再修訂,參見 P1002R0) - 沒有宣告或使用非 Literal Type 的變數或者使用 Thread Local Storage
如果一個 Lambda Expression 滿足上述規定,即使該 Lambda Expression 沒有被 constexpr
關鍵字修飾也能自動地被視為 Constexpr Lambda Expression:
void test() {
auto fib = [](int n) -> int { // changed
int a = 0, b = 1;
for (int i = 0; i < n; ++i) {
int tmp = a + b;
a = b;
b = tmp;
}
return a;
};
static_assert(fib(5) == 5, "");
static_assert(fib(6) == 8, "");
}
Constexpr Lambda Expression 與編譯期錯誤
和 Constexpr Function 一樣,「Constexpr Lambda Expression 的參數」與「Constexpr Lambda Expression 捕捉的變數」都有可能不是編譯期常數。在那個情況下,Constexpr Lambda Expression 會退化為普通的 Lambda Expression:
#include <iostream>
int main() {
int x = 0;
std::cin >> x;
auto f = [x](int a, int b) constexpr {
return a * x + b;
};
std::cout << f(2, 3) << std::endl;
}
當然,如果 Constexpr Lambda Expression 捕捉的變數不是編譯期常數,對應的 Closure Object 就不會是編譯期常數。以下的 Lambda Expression 捕捉了區域變數 x
。因為 x
不是編譯期常數,Lambda Expression 產生的 Closure Object 也不會是編譯期常數。當我們將 Closure Object 指派給以 constexpr
修飾的區域變數 f
的時候,編譯器會回報錯誤:
int main() {
int x = 0;
constexpr auto f = [x]() { return x; }; // compile-time error
}
下面的範例雖然比較複雜,但也是相同的道理。因為 Constexpr Function 在參數不是編譯期常數的時候會退化為普通函式,那時候就不能把 Closure Object 指挀給以 constexpr
修飾的區域變數 f
:
constexpr int test(int x) {
constexpr auto f = [x] { return x; }; // compile-time error
return f();
}
以下程式碼有考慮退化的情況,test
函式的區域變數 f
可以接收「編譯期常數」與「一般數值」,所以可以正常運作:
#include <iostream>
constexpr int test(int x, int a, int b) {
auto f = [x](int y) constexpr {
return x * y;
};
return f(a) + f(b);
}
int main() {
static_assert(test(1, 2, 3) == 5, "");
static_assert(test(4, 5, 6) == 44, "");
int a = 0;
int b = 0;
int c = 0;
std::cin >> a >> b >> c;
std::cout << test(a, b, c) << std::endl;
}
另外,如果編譯器在編譯期計算 Constexpr Lambda Expression 函式呼叫的過程中遇到「需要執行期資訊」的語言構件[2],編譯器會產生編譯期錯誤。例如:throw
述句或者 new
表達式都會造成編譯錯誤:
void test_compile_error() {
auto f = [](int i) constexpr -> const char * {
if (i > 0) {
return new char[i]; // #1
}
throw i; // #2
};
static_assert(f(1) != nullptr, ""); // compile-time error #1
static_assert(f(0) != nullptr, ""); // compile-time error #2
}
不過如果計算 Constexpr Lambda Expression 函式呼叫的過程不會執行到「需要執行期資訊」的語言構件,就不會產生問題:
void test_ok() {
auto f = [](int a, int b) constexpr {
if (b == 0) {
throw 0; // not reached by f(13, 5)
}
return a / b;
};
static_assert(f(13, 5) == 2, ""); // fine
}
參考資料
- P0170R1: Wording for Constexpr Lambda
- N4487: Constexpr Lambda
- N3337: Working Draft, Standard for Programming Language C++ (C++ 11 Draft); Section 5.19. Constant expressions; Section 7.1.5. The constexpr specifier
- N4140: Working Draft, Standard for Programming Language C++ (C++ 14 Draft); Section 5.19. Constant expressions; Section 7.1.5. The constexpr specifier
- N4659: Working Draft, Standard for Programming Language C++ (C++ 17 Draft); Section 8.1.5. Lambda expressions; Section 8.20. Constant expressions; Section 10.1.5. The constexpr specifier
- P1018R1: Evolution status after Rapperswil 2018, "P1002R0, Try-catch blocks in constexpr functions was approved. A constexpr function can contain a try-catch, and constant evaluation is okay as long as an exception isn't thrown," cited from P1018R1.
- P1002R0: Try-catch blocks in constexpr functions
[1] | 關於 Constexpr Function 的要求,請參閱 N4659 第 162 頁第 10.1.5 節 The constexpr specifier [dcl.constexpr] 第 3 款。 |
[2] | 關於不能於 Constexpr Lambda Expression 內部使用的語言構件,請參閱 N4659 第 139 頁第 8.20 節 Constant expressions [expr.const] 第 2 款。 |
Note
如果你喜歡這篇文章,請追蹤羅根學習筆記粉絲專頁。最新文章都會在粉絲專頁發佈通知。