第 18.12 節
std::function
0瀏覽次數0訪問次數--跳出率--平均停留
本節解決什麼問題
普通函數、函數指針、Lambda、函數對象都可以"被調用",它們統稱為可調用對象。但它們的具體類型並不一樣:
- 普通函數有函數類型。
- 函數指針是指針類型。
- 每個 Lambda 都有編譯器生成的獨立類型。
- 函數對象是自定義類類型。
如果你只是立刻調用一次,類型不同通常不是問題;但如果你要把回調保存到成員變量裡,或者把多個不同回調放進同一個容器裡,就需要一個統一類型。
std::function 就是標準庫提供的通用可調用對象包裝器。
這個特性是什麼
std::function<返回值(参数列表)> 可以保存任何簽名匹配的可調用對象。
例如 std::function<int(int, int)> 表示:保存一個可以用兩個 int 調用,並返回 int 的可調用對象。
C++ 標準版本
std::function 從 C++11 開始提供,需要頭文件 <functional>。
常見場景
| 場景 | 是否適合 std::function | 原因 |
|---|---|---|
| 函數里立刻調用一次回調 | 不一定 | 模板參數通常更輕量 |
| 類成員變量保存一個回調 | 適合 | 成員變量需要穩定類型 |
vector 保存多個不同 lambda | 適合 | 容器元素必須是同一種類型 |
| 高頻性能熱點裡的小函數 | 謹慎 | std::function 有類型擦除開銷 |
| 只保存無捕獲函數 | 函數指針也可以 | 但函數指針不能保存有捕獲 lambda |
示例代碼
示例 1:統一保存不同類型的可調用對象
#include <functional>
#include <iostream>
int add(int a, int b)
{
return a + b;
}
struct Multiply
{
int operator()(int a, int b) const
{
return a * b;
}
};
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
// std::function 可以保存普通函数、lambda 或函数对象。
std::function<int(int, int)> op;
op = add;
std::cout << "add: " << op(3, 4) << "\n";
int (*function_pointer)(int, int) = add;
op = function_pointer;
std::cout << "function pointer: " << op(5, 6) << "\n";
op = [](int a, int b) {
return a - b;
};
std::cout << "lambda: " << op(10, 3) << "\n";
op = Multiply{};
std::cout << "function object: " << op(7, 8) << "\n";
return 0;
}
運行結果:
add: 7
function pointer: 11
lambda: 7
function object: 56
示例 2:把 std::function 作為參數傳遞迴調
這個例子中,calculate 不關心具體傳進來的是普通函數還是 lambda,只要求籤名是 int(int, int)。
#include <functional>
#include <iostream>
int add(int a, int b)
{
return a + b;
}
// std::function 可以保存普通函数、lambda 或函数对象。
int calculate(int a, int b, const std::function<int(int, int)>& op)
{
return op(a, b);
}
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
int x = 10;
int y = 5;
int r1 = calculate(x, y, add);
std::cout << "add result = " << r1 << "\n";
int r2 = calculate(x, y, [](int a, int b) {
return a * b;
});
std::cout << "multiply result = " << r2 << "\n";
int offset = 100;
int r3 = calculate(x, y, [offset](int a, int b) {
return a + b + offset;
});
std::cout << "with offset result = " << r3 << "\n";
return 0;
}
運行結果:
add result = 15
multiply result = 50
with offset result = 115
示例 3:把回調保存到類成員變量
函數指針不能保存有捕獲的 lambda;std::function 可以,所以它非常適合保存事件回調。
#include <functional>
#include <iostream>
#include <string>
#include <utility>
class Button
{
// std::function 可以保存普通函数、lambda 或函数对象。
std::function<void()> on_click_;
public:
void set_on_click(std::function<void()> callback)
{
on_click_ = std::move(callback);
}
void click() const
{
if (on_click_)
{
on_click_();
}
else
{
std::cout << "no callback\n";
}
}
};
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
Button button;
button.click();
std::string name = "Save";
int count = 0;
button.set_on_click([name, &count]() {
++count;
std::cout << name << " clicked, count = " << count << "\n";
});
button.click();
button.click();
return 0;
}
運行結果:
no callback
Save clicked, count = 1
Save clicked, count = 2
這裡按值捕獲 name,按引用捕獲 count。因為 button 和 count 都在 main 中,count 活得比回調調用更久,所以這個例子是安全的。真實異步場景中,保存回調時要更謹慎地處理生命週期。
示例 4:把多個不同回調放進同一個容器
每個 Lambda 的類型都不同,不能直接放進同一個 vector。用 std::function<void()> 之後,它們就有了統一類型。
#include <functional>
#include <iostream>
#include <string>
#include <vector>
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
// std::function 可以保存普通函数、lambda 或函数对象。
// vector 是动态数组,元素数量可以在运行时变化。
std::vector<std::function<void()>> tasks;
int total = 0;
std::string label = "task";
tasks.push_back([label]() {
std::cout << label << " A\n";
});
tasks.push_back([&total]() {
total += 10;
std::cout << "add 10, total = " << total << "\n";
});
tasks.push_back([&total]() {
total *= 2;
std::cout << "double, total = " << total << "\n";
});
for (const auto& task : tasks)
{
task();
}
std::cout << "final total = " << total << "\n";
return 0;
}
運行結果:
task A
add 10, total = 10
double, total = 20
final total = 20
示例 5:隻立刻調用時,模板參數也可以接收回調
std::function 的優勢是"保存和統一類型"。如果只是立刻調用,不需要保存,函數模板通常更輕量。
#include <iostream>
template<typename Callback>
void repeat(int times, Callback callback)
{
for (int i = 0; i < times; ++i)
{
callback(i);
}
}
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
repeat(3, [](int i) {
std::cout << "i = " << i << "\n";
});
// 返回 0 表示程序正常结束。
return 0;
}
運行結果:
i = 0
i = 1
i = 2
關鍵語法解釋
| 寫法 | 含義 | 注意事項 |
|---|---|---|
std::function<void()> | 保存無參數、無返回值的回調 | 常用於按鈕、定時器、任務隊列 |
std::function<int(int, int)> | 保存兩個 int 參數、返回 int 的可調用對象 | 簽名必須匹配 |
if (callback) | 判斷 std::function 是否為空 | 空回調不能直接調用 |
std::move(callback) | 把傳入的回調移動到成員變量 | 避免不必要拷貝 |
vector<std::function<void()>> | 保存多個不同類型的回調 | 容器元素類型統一 |
std::function 和 Lambda 的關係
Lambda 是一種可調用對象,std::function 是用來保存可調用對象的包裝器。它們不是互相替代的關係。
| 需求 | 推薦 |
|---|---|
| 就地寫一段短邏輯 | Lambda |
| 把回調保存成變量或成員 | std::function |
| 把多個不同 lambda 放入容器 | std::function |
| 調用一次且性能敏感 | 函數模板 |
常見錯誤
- 空的
std::function直接調用。調用前先判斷if (callback)。 - 簽名不匹配。比如回調需要
int(int, int),就不能傳入只接收一個參數的可調用對象。 - 保存回調時引用捕獲了已經銷燬的局部變量。
- 只需要臨時調用一次,也把參數寫成
std::function,在性能敏感代碼中會產生不必要開銷。 - 以為成員函數可以直接賦給
std::function<void(int)>。非靜態成員函數還需要對象,下一節會用std::bind和 Lambda 解決這個問題。
使用建議
- 需要保存回調時,用
std::function。 - 需要統一不同類型的回調時,用
std::function。 - 只是立刻調用一次回調時,可以優先考慮模板參數。
- 保存回調時特別注意捕獲對象的生命週期。
- 不要把
std::function當成所有回調場景的唯一答案,它是清晰性和靈活性的工具,也有一定運行期開銷。
小結
- 可調用對象包括普通函數、函數指針、Lambda、函數對象等。
std::function<返回值(参数...)>用統一類型保存簽名匹配的可調用對象。std::function適合保存回調、作為成員變量、放入容器。- 空的
std::function不能調用。 - 只臨時調用一次的回調,模板參數通常更輕量。