跳到正文

Wiki

std::function

約 5 分鐘閱讀

本文由簡體中文內容確定性轉換,並受版本化術語表保護。

1.本節解決什麼問題

普通函式、函式指標、Lambda、函式物件都可以"被呼叫",它們統稱為可呼叫物件。但它們的具體型別並不一樣:

  • 普通函式有函式型別。
  • 函式指標是指標型別。
  • 每個 Lambda 都有編譯器生成的獨立型別。
  • 函式物件是自定義類型別。

如果你只是立刻呼叫一次,型別不同通常不是問題;但如果你要把回撥儲存到成員變數裡,或者把多個不同回撥放進同一個容器裡,就需要一個統一型別。

std::function 就是標準庫提供的通用可呼叫物件包裝器。

2.這個特性是什麼

std::function<返回值(参数列表)> 可以儲存任何簽名匹配的可呼叫物件。

例如 std::function<int(int, int)> 表示:儲存一個可以用兩個 int 呼叫,並返回 int 的可呼叫物件。

3.C++ 標準版本

std::function 從 C++11 開始提供,需要標頭檔案 <functional>

4.常見場景

場景 是否適合 std::function 原因
函數里立刻呼叫一次回撥 不一定 模板引數通常更輕量
類成員變數儲存一個回撥 適合 成員變數需要穩定型別
vector 儲存多個不同 lambda 適合 容器元素必須是同一種類型
高頻效能熱點裡的小函式 謹慎 std::function 有型別擦除開銷
只儲存無捕獲函式 函式指標也可以 但函式指標不能儲存有捕獲 lambda

5.示例程式碼

5.1.示例 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

5.2.示例 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

5.3.示例 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。因為 buttoncount 都在 main 中,count 活得比回撥呼叫更久,所以這個例子是安全的。真實非同步場景中,儲存回撥時要更謹慎地處理生命週期。

5.4.示例 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.5.示例 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

6.關鍵語法解釋

寫法 含義 注意事項
std::function<void()> 儲存無引數、無返回值的回撥 常用於按鈕、定時器、任務佇列
std::function<int(int, int)> 儲存兩個 int 引數、返回 int 的可呼叫物件 簽名必須匹配
if (callback) 判斷 std::function 是否為空 空回撥不能直接呼叫
std::move(callback) 把傳入的回撥移動到成員變數 避免不必要複製
vector<std::function<void()>> 儲存多個不同型別的回撥 容器元素型別統一

7.std::function 和 Lambda 的關係

Lambda 是一種可呼叫物件,std::function 是用來儲存可呼叫物件的包裝器。它們不是互相替代的關係。

需求 推薦
就地寫一段短邏輯 Lambda
把回撥儲存成變數或成員 std::function
把多個不同 lambda 放入容器 std::function
呼叫一次且效能敏感 函式模板

8.常見錯誤

  1. 空的 std::function 直接呼叫。呼叫前先判斷 if (callback)
  2. 簽名不匹配。比如回撥需要 int(int, int),就不能傳入只接收一個引數的可呼叫物件。
  3. 儲存回撥時引用捕獲了已經銷燬的區域性變數。
  4. 只需要臨時呼叫一次,也把引數寫成 std::function,在效能敏感程式碼中會產生不必要開銷。
  5. 以為成員函式可以直接賦給 std::function<void(int)>。非靜態成員函式還需要物件,下一節會用 std::bind 和 Lambda 解決這個問題。

9.使用建議

  1. 需要儲存回撥時,用 std::function
  2. 需要統一不同型別的回撥時,用 std::function
  3. 只是立刻呼叫一次回撥時,可以優先考慮模板引數。
  4. 儲存回撥時特別注意捕獲物件的生命週期。
  5. 不要把 std::function 當成所有回撥場景的唯一答案,它是清晰性和靈活性的工具,也有一定執行期開銷。

10.小結

  • 可呼叫物件包括普通函式、函式指標、Lambda、函式物件等。
  • std::function<返回值(参数...)> 用統一型別儲存簽名匹配的可呼叫物件。
  • std::function 適合儲存回撥、作為成員變數、放入容器。
  • 空的 std::function 不能呼叫。
  • 只臨時呼叫一次的回撥,模板引數通常更輕量。