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++ 11C++ 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
  •