如果一個成員函式(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()();
}
};