文章目錄
- 一、按引用捕獲和按值捕獲
- 1.1 原理
- 1.2 案例
- 二、弱引用
- 2.1 原理
- 2.2 案例一
- 2.3 案例二:使用`base`庫的弱引用
- 三、總結
在C++回調中,當使用Lambda表達式捕獲外部變量時,有兩種捕獲方式:按值捕獲和按引用捕獲。
一、按引用捕獲和按值捕獲
1.1 原理
-
按引用捕獲是將外部變量的引用存儲在Lambda表達式的閉包中,
[&]
表示按引用捕獲所有外部變量。這樣,當Lambda表達式執行時,它將直接訪問原始變量。這種方式在某些情況下可能導致問題,例如,當回調執行時,原始變量已經失效(例如,原始變量是棧上的局部變量,而回調在該變量離開作用域后執行)。 -
按值捕獲是將外部變量的值復制到Lambda表達式的閉包中。這樣,當Lambda表達式執行時,它將使用這個復制的值,而不是原始變量的值。這種方式可以避免在回調執行時,原始變量已經失效的問題。
1.2 案例
原理雖然很簡單,但是當我們處于復雜的業務代碼中時,仍然不免會寫出bug。下面是筆者遇到的一個真實案例:
std::string WebProxyKeysHelper::GetAuthCode(std::string &st_or_code, std::string &last_key) {...auto ph = (st_or_code == KEY_TYPE_OF_ST_STR ? RefreshProxySt() : RefreshOauthCode());auto prom_ptr = std::make_shared<std::promise<std::string>>();std::future<std::string> fut_pb = prom_ptr->get_future();ph.then([&, prom_ptr](bool ret) {std::string tmp_key = "";if (ret) {tmp_key = st_or_code == KEY_TYPE_OF_ST_STR ? proxy_st_ : oauth_code_;UpdateKeys(st_or_code, tmp_key);Schedule();}prom_ptr->set_value(tmp_key);}).onError([&, prom_ptr](const std::exception& ex){prom_ptr->set_value("");});}...return current_key;}
在上述代碼中,WebProxyKeysHelper::GetAuthCode
函數通過異步操作 ph
獲取代理密鑰。然后,根據異步操作的結果,回調函數更新密鑰并設置 prom_ptr
的值。然而,這段代碼存在一個潛在的問題,即在回調函數中使用了按引用捕獲的 st_or_code
變量。
問題在于,當 ph.then([&, prom_ptr](bool ret) { ... })
回調執行時,st_or_code
變量可能已經離開了作用域并被銷毀。這會導致程序偶現閃退,也可能導致數值異常,最終表現為業務邏輯異常,因為回調函數試圖訪問一個已經失效的棧變量。
修改的方式是,將 st_or_code
變量改為按值捕獲。這樣,在回調執行時,即使原始的 st_or_code
變量離開了作用域,回調中仍然可以安全地使用其復制的值。下面是修正后的代碼:
std::string WebProxyKeysHelper::GetAuthCode(std::string &st_or_code, std::string &last_key) {...auto ph = (st_or_code == KEY_TYPE_OF_ST_STR ? RefreshProxySt() : RefreshOauthCode());auto prom_ptr = std::make_shared<std::promise<std::string>>();std::future<std::string> fut_pb = prom_ptr->get_future();ph.then([&, st_or_code, prom_ptr](bool ret) { // 注意這里改為按值捕獲 st_or_codestd::string tmp_key = "";if (ret) {tmp_key = st_or_code == KEY_TYPE_OF_ST_STR ? proxy_st_ : oauth_code_;UpdateKeys(st_or_code, tmp_key);Schedule();}prom_ptr->set_value(tmp_key);}).onError([&, prom_ptr](const std::exception& ex){prom_ptr->set_value("");});}...return current_key;
}
二、弱引用
2.1 原理
弱引用(Weak Reference)是一種特殊的引用類型,它不會阻止其所引用的對象被垃圾回收。這在處理回調和長時間運行的任務時非常有用,因為它可以避免因為回調導致的潛在內存泄漏。
2.2 案例一
錯誤的寫法:
class Foo {
public:void start() {std::thread t([this]() {std::this_thread::sleep_for(std::chrono::seconds(1));this->doSomething(); // Undefined behavior if `this` is destroyed!});t.detach();}void doSomething() {std::cout << "Doing something..." << std::endl;}
};
在上述代碼中,我們在新線程中訪問了this指針。然而,如果新線程開始執行時,this指針所指向的對象已經被銷毀,這將導致未定義的行為。
正確的寫法:
class Foo : public std::enable_shared_from_this<Foo> {
public:void start() {std::thread t([weak_this = std::weak_ptr<Foo>(shared_from_this())]() {if (auto shared_this = weak_this.lock()) {shared_this->doSomething(); // 安全,只要 `this` 沒有被銷毀}});t.detach();}void doSomething() {std::cout << "Doing something..." << std::endl;}
};
在修正的代碼中,我們使用了弱引用來捕獲this指針。這樣,即使原始對象被銷毀,新線程中也不會訪問到無效的this指針。
2.3 案例二:使用base
庫的弱引用
base::BindLambda(base::AsWeakPtr(this), [&] { ... })
使用了弱引用。這里,base::AsWeakPtr(this)
將this
指針轉換為弱引用,并將其傳遞給Lambda表達式。這樣,在回調執行時,如果this
指針所指向的對象已經被銷毀,回調將不會執行,從而避免了潛在的內存泄漏問題。
下面是執行CGI任務時的回調寫法。當CGI網絡請求回來時,所在的Service類可能已經被析構,所以需要使用base::AsWeakPtr(this)
將this
指針轉換為弱引用:
task->SetCallback(base::BindLambda(base::AsWeakPtr(this), [=](network::ProtocolErrorCode pec, const CRTX_WWK::BatchSetLeaderRsp& resp) {LogicErrorCode code = (pec == network::PEC_OK && task->response_head()->errcode() == 0) ? LEC_OK : LEC_ERROR;if(code == LEC_OK) {...}if (!callback.is_null()) callback.Run(code);
}));
ScheduleTask(task.get());
大家可能已經注意到,上面的Lamda回調中,我們不需要再額外判斷this
是否已經被析構,因為base庫已經替我們提前判斷好再回調:
/*** @brief BindLambda 函數實現了便捷的通過 C++ Lambda 表達式來創建 base::Callback 的方法。* 這個重載允許額外傳入一個 base::WeakPtr 類型的弱引用,在實際執行 functor 前會檢查弱引用的有效性,如果弱引用已經無效,則不會執行 functor。** @param weakptr 額外傳遞一個弱引用,在 functor 執行前會進行檢查,如果該弱引用無效則不會繼續調用 functor* @param functor C++ Lambda 表達式* @param params 需要綁定在 Lambda 表達式上的參數** @note 可根據實際情況,選擇使用捕獲或者綁定的方式傳遞參數。*/
template <typename SupportWeakPtrType, typename Functor, typename ...Params>
auto BindLambda(const WeakPtr<SupportWeakPtrType>& weakptr, const Functor& functor, const Params&... params) -> decltype(BindLambda(functor, params...)) {return _WrapWeakCallback(BindLambda(functor, params...), weakptr);
}template <typename SupportWeakPtrType, typename RetType, typename ...Params>
base::Callback<RetType(Params...)> _WrapWeakCallback(const base::Callback<RetType(Params...)>& callback, const WeakPtr<SupportWeakPtrType>& weakptr) {return base::Bind(&_RunWeakCallbackInternalRet<SupportWeakPtrType, RetType, Params...>, weakptr, callback);
}template <typename SupportWeakPtrType, typename RetType, typename ...Params>
RetType _RunWeakCallbackInternalRet(const WeakPtr<SupportWeakPtrType>& weakptr, const base::Callback<RetType(Params...)>& callback, Params... params) {if (weakptr.get()) {return callback.Run(params...);}return RetType();
}
-
BindLambda
函數接受一個弱引用(weakptr
)、一個Lambda表達式(functor
)和一些參數(params
)。它將創建一個回調函數,該回調在執行前會檢查弱引用的有效性。如果弱引用無效,則不會執行Lambda表達式。 -
_WrapWeakCallback
函數接受一個回調函數(callback
)和一個弱引用(weakptr
)。它將創建一個新的回調函數,該回調函數在調用之前會檢查弱引用的有效性。 -
_RunWeakCallbackInternalRet
函數在弱引用有效時執行回調函數(callback
),否則返回默認值。這個函數實際上是在執行回調之前檢查弱引用的有效性的地方。
三、總結
在C++回調中,我們需要根據具體情況選擇合適的捕獲方式(按值捕獲、按引用捕獲或弱引用)。在處理回調和長時間運行的任務時,為了避免內存泄漏和訪問無效變量的問題,我們通常需要使用按值捕獲和弱引用。
最后我們總結一下本文:
類型 | 原理 | 注意事項 |
---|---|---|
按值捕獲 | 將外部變量的值復制到Lambda表達式的閉包中,使得Lambda表達式在執行時使用的是復制的值,而不是原始變量的值。 | 如果捕獲的變量在Lambda表達式執行時已經離開了作用域,那么按值捕獲就是安全的,因為Lambda表達式中使用的是變量的副本。 |
按引用捕獲 | 將外部變量的引用存儲在Lambda表達式的閉包中,使得Lambda表達式在執行時直接訪問的是原始變量。 | 如果捕獲的變量在Lambda表達式執行時已經離開了作用域,那么按引用捕獲就可能導致未定義的行為。因此,使用按引用捕獲時,需要確保捕獲的變量在Lambda表達式執行時仍然有效。 |
弱引用 | 弱引用是一種特殊的引用類型,它不會阻止其所引用的對象被垃圾回收。這在處理回調和長時間運行的任務時非常有用,因為它可以避免因為回調導致的潛在內存泄漏。 | 如果弱引用所引用的對象在回調執行時已經被銷毀,那么回調將不會執行,從而避免了潛在的內存泄漏問題。因此,使用弱引用時,需要確保在回調執行時,弱引用所引用的對象仍然存在。 |