C++ 17 在 Lambda Expression 捕捉 *this

如果一個成員函式(Member Function)有 Lambda Expression 而且該 Lambda Expression 有指定預設補捉模式(不論是傳值或傳參考)。當 Lambda Expression 內有程式碼指稱「成員函式」或「資料成員」時,this 會被隱含地加入 Capture List。Lambda Expression 在執行時會透過 this 指標存取成員函式或資料成員:

#include <cassert>

class Example {
private:
  int value_;
public:
  Example(int value = 0) : value_(value) {}

  void run() {
    auto f = [&]() { ++value_; };
    auto g = [=]() { ++value_; };

    assert(value_ == 0);
    f();
    assert(value_ == 1);
    g();
    assert(value_ == 2);
  }
};

int main() {
  Example x;
  x.run();
  return 0;
}

然而在一些情境下,我們會希望被補捉的不只是 this 指標。我們想要捕捉整個 this 指向的物件。例如以下 C++ 11 程式碼:

#include <cmath>
#include <future>
#include <iostream>

class Task {
private:
  double a_;
  double b_;

public:
  Task() : a_(0.0), b_(0.0) {}

  ~Task() {
    // Clear data members to show use-after-free problem
    a_ = std::nan("");
    b_ = std::nan("");
  }

  void set_param_a(double a) { a_ = a; }
  void set_param_b(double b) { b_ = b; }

  std::future<double> run() {
    Task self = *this;
    return std::async(std::launch::async, [=]() {
      return std::sqrt(self.a_ * self.a_ + self.b_ * self.b_);
    });
  }
};

std::future<double> start() {
  Task t;
  t.set_param_a(5.0);
  t.set_param_b(12.0);
  return t.run();
}

int main() {
  std::cout << start().get() << std::endl;
}

首先我們先看 run 函式。假設 run 函式要進行複雜的運算,所以使用 std::async 函式將計算工作交給另一個執行緒(Thread)。當另一個執行緒完成計算後,會將計算結果填入回傳的 std::future<double> 物件。因為計算過程要使用 Task 的成員變數,所以我們先將 *this 複製為 self,再讓 Lambda Expression 捕捉 self 變數。

在這個例子中,我們不能直接捕捉 this,因為被捕捉的 this 只是指標。受限於 start 函式的實作,this 指向的物件有可能會在 Lambda Expression 執行之前就被解構。

在 C++ 14 run 函式能直接利用「Lambda Capture Initializer」初始化 self 並減少一次複製建構(如下)。然而我們仍需要透過 self. 存取成員變數。如果我們忘記加上 self.,編譯器會自動捕捉 this 指標,進而產生非預期的錯誤。

std::future<double> run() {
  return std::async(std::launch::async, [=, self=*this]() {
    return std::sqrt(self.a_ * self.a_ + self.b_ * self.b_);
  });
}

在 C++ 17,我們能直接在 Lambda Expression 的 Capture List 指定 *this。這代表我們要將 this 指標指向的物件以傳值的方式傳入 Lambda Expression。在 Lambda Expression 內,我們能直接以資料成員的名稱(本例中分別為 a_b_)存取資料成員:

std::future<double> run() {
  return std::async(std::launch::async, [=, *this]() {
    return std::sqrt(a_ * a_ + b_ * b_);
  });
}

Capture List 規則

在 C++ 17 Capture List 與 this 的規則改為:

  • [&] 隱含地抓取 this 指標。
  • [=] 隱含地抓取 this 指標。
  • [&, this] 明確地抓取 this 指標。與 [&] 效果相同。
  • [=, *this] 明確地抓取「this 指標指向的物件」。
  • [=, this] 在 C++ 20 是明確地抓取 this 指標。與 [=] 相同。如果你只想捕捉 this 指標,而不是「this 指標指向的物件」,C++ 20 建議改用這個寫法。然而在 C++ 17 這是錯誤語法。(參見 P0409R2
  • [this, *this] 是錯誤語法。

另外,在一個巢狀 Lambda Expression 之中,如果一個 Lambda Expression 想要捕捉 this 指標或 *this,上一層的 Lambda Expression 也必需捕捉 this*this

class ExampleNested {
private:
  int value_;

public:
  ExampleNested(int value = 0) : value_(value) {}

  int test() {
    auto f = [this]() {
      return [*this]() {
        return value_;
      };
    };

    auto g = [*this]() {
      return [this]() {
        return value_;
      };
    };

    auto h = [*this]() {
      return [*this]() {
        return value_;
      };
    };
    return f()() + g()() + h()();
  }
};

而以下程式碼會產生編譯錯誤:

class ExampleNestedBad {
private:
  int value_;

public:
  ExampleNestedBad(int value = 0) : value_(value) {}

  int test() {
    auto f = []() {
      return [*this]() {  // error: `this` is not captured by f
        return value_;
      };
    };
    return f()();
  }
};

參考資料

  • P0018R3: Lambda Capture of *this by Value as [=,*this]
  • P0409R2: Allow lambda capture [=, this]

Note

如果你喜歡這篇文章,請追蹤羅根學習筆記粉絲專頁。最新文章都會在粉絲專頁發佈通知。