跳到正文

Wiki

std::bind

約 6 分鐘閱讀

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

1.本節解決什麼問題

有時你已經有一個函式,但它的引數和目標介面不匹配:

  • 想提前固定某些引數。
  • 想調整引數順序。
  • 想把成員函式繫結到某個物件上,變成普通回撥。

std::bind 可以把已有可呼叫物件適配成新的可呼叫物件。

現代 C++ 中,很多 bind 場景可以用 Lambda 寫得更清楚。不過 std::bind 在舊專案、ROS2 示例、Boost.Asio 回撥、成員函式適配中仍然經常出現,值得掌握。

2.這個特性是什麼

std::bind 接收一個可呼叫物件和一組繫結引數,返回一個新的可呼叫物件。呼叫新物件時,佔位符 _1_2_3 表示"呼叫時傳進來的第 1、2、3 個引數"。

std::bind 需要標頭檔案 <functional>,佔位符在名稱空間 std::placeholders 中。

寫法 含義
std::bind(f, 10, _1) 固定 f 的第一個引數為 10
std::bind(f, _2, _1) 調換呼叫時兩個引數的順序
std::bind(&C::m, &obj, _1) 把成員函式繫結到物件 obj
std::bind(f, std::ref(x), _1) 繫結引用,而不是複製

3.示例程式碼

3.1.示例 1:固定普通函式的部分引數

#include <functional>
#include <iostream>
#include <string>

void print_student(const std::string& name, int age, int score)
{
    std::cout << name << " age=" << age << " score=" << score << "\n";
}

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    using std::placeholders::_1;
    using std::placeholders::_2;

    print_student("Alice", 20, 95);

    // bind 会把函数和部分参数提前绑定成一个可调用对象。
    auto print_bob = std::bind(print_student, "Bob", _1, _2);
    print_bob(21, 88);

    auto print_charlie_score = std::bind(print_student, "Charlie", 22, _1);
    print_charlie_score(91);

    return 0;
}

執行結果

Alice age=20 score=95
Bob age=21 score=88
Charlie age=22 score=91

3.2.示例 2:調整引數順序

佔位符的位置決定了原函式接收到的引數順序。

#include <functional>
#include <iostream>

void divide(int a, int b)
{
    std::cout << a << " / " << b << " = " << (a / b) << "\n";
}

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    using std::placeholders::_1;
    using std::placeholders::_2;

    divide(10, 2);

    // bind 会把函数和部分参数提前绑定成一个可调用对象。
    auto swapped = std::bind(divide, _2, _1);
    swapped(2, 10);

    auto half = std::bind(divide, _1, 2);
    half(10);
    half(20);

    return 0;
}

執行結果

10 / 2 = 5
10 / 2 = 5
10 / 2 = 5
20 / 2 = 10

3.3.示例 3:繫結成員函式

非靜態成員函式需要物件才能呼叫。std::bind 的第二個引數通常就是物件指標、物件引用或智慧指標。

#include <functional>
#include <iostream>
#include <string>

class Printer
{
    std::string prefix_;

public:
    explicit Printer(const std::string& prefix) : prefix_(prefix) {}

    void print(int id, const std::string& text) const
    {
        std::cout << prefix_ << " #" << id << ": " << text << "\n";
    }
};

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    using std::placeholders::_1;
    using std::placeholders::_2;

    Printer printer("LOG");

    // bind 会把函数和部分参数提前绑定成一个可调用对象。
    auto print_any = std::bind(&Printer::print, &printer, _1, _2);
    print_any(7, "ready");

    auto print_ok = std::bind(&Printer::print, &printer, _1, "ok");
    print_ok(8);

    return 0;
}

執行結果

LOG #7: ready
LOG #8: ok

這裡 &printer 必須在 print_anyprint_ok 使用期間一直有效。如果繫結後的回撥會被儲存到更久的地方,通常要考慮智慧指標或更明確的生命週期管理。

3.4.示例 4:bind 和 Lambda 的對比

同一個引數適配任務,Lambda 往往更直觀,因為引數名和呼叫關係都寫得很清楚。

#include <functional>
#include <iostream>

int weighted_sum(int base, int x, int y)
{
    return base + x * 10 + y;
}

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    using std::placeholders::_1;
    using std::placeholders::_2;

    // bind 会把函数和部分参数提前绑定成一个可调用对象。
    auto by_bind = std::bind(weighted_sum, 100, _1, _2);
    auto by_lambda = [](int x, int y) {
        return weighted_sum(100, x, y);
    };

    std::cout << "bind result = " << by_bind(3, 4) << "\n";
    std::cout << "lambda result = " << by_lambda(3, 4) << "\n";

    auto swapped_bind = std::bind(weighted_sum, 100, _2, _1);
    auto swapped_lambda = [](int x, int y) {
        return weighted_sum(100, y, x);
    };

    std::cout << "swapped bind result = " << swapped_bind(3, 4) << "\n";
    std::cout << "swapped lambda result = " << swapped_lambda(3, 4) << "\n";

    return 0;
}

執行結果

bind result = 134
lambda result = 134
swapped bind result = 143
swapped lambda result = 143

3.5.示例 5:bind 預設複製引數,需要引用時用 std::ref

std::bind 儲存普通引數時預設複製。如果你要繫結引用,必須顯式使用 std::refstd::cref

#include <functional>
#include <iostream>

void add_to(int& total, int value)
{
    total += value;
    std::cout << "inside total = " << total << "\n";
}

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    int total = 0;

    // bind 会把函数和部分参数提前绑定成一个可调用对象。
    auto add_copy = std::bind(add_to, total, 5);
    add_copy();
    std::cout << "outside after copy bind = " << total << "\n";

    auto add_ref = std::bind(add_to, std::ref(total), 5);
    add_ref();
    std::cout << "outside after ref bind = " << total << "\n";

    return 0;
}

執行結果

inside total = 5
outside after copy bind = 0
inside total = 5
outside after ref bind = 5

3.6.示例 6:在成員函式中使用 this 綁定當前物件

前面的示例是在類外部建立物件,再把物件地址 &printer 傳給 std::bind。如果 std::bind 本身就在類的成員函式中使用,可以直接傳入 this,表示把成員函式繫結到當前物件。

#include <functional>
#include <iostream>
#include <string>

class Robot
{
    std::string name_;

public:
    explicit Robot(const std::string& name) : name_(name) {}

    void on_message(int id, const std::string& text)
    {
        std::cout << name_ << " #" << id << ": " << text << "\n";
    }

    void demo()
    {
        using std::placeholders::_1;
        using std::placeholders::_2;

        auto callback = std::bind(&Robot::on_message, this, _1, _2);

        callback(7, "ready");
        callback(8, "running");
    }
};

int main()
{
    Robot robot("Cyber");
    robot.demo();

    return 0;
}

執行結果

Cyber #7: ready
Cyber #8: running

這裡最重要的是:

auto callback = std::bind(&Robot::on_message, this, _1, _2);

把它拆開看:

引數 含義
&Robot::on_message 要呼叫的成員函式
this 呼叫這個成員函式的當前物件
_1 callback() 呼叫時傳入的第一個引數
_2 callback() 呼叫時傳入的第二個引數

非靜態成員函式不能脫離物件單獨呼叫。&Robot::on_message 只說明瞭“要呼叫哪個成員函式”,還必須告訴 std::bind“在哪個 Robot 物件上呼叫”。

在類外部,可以寫:

std::bind(&Robot::on_message, &robot, _1, _2);

而在 Robot 自己的成員函式內部,this 就是當前物件的指標,所以可以直接寫:

std::bind(&Robot::on_message, this, _1, _2);

例如:

Robot robot("Cyber");
robot.demo();

進入 robot.demo() 後,demo() 內部的 this 就指向 robot

因此:

callback(7, "ready");

可以近似理解成:

this->on_message(7, "ready");

如果當前物件就是 robot,也就相當於:

robot.on_message(7, "ready");

所以這類寫法可以記成:

std::bind(&类名::成员函数, this, 参数...);

其中第二個引數 this 的作用是:

告訴 std::bind:以後呼叫這個成員函式時,就在當前這個物件上呼叫。

這也是 ROS2 和 Boost.Asio 中很常見的寫法,例如:

std::bind(&MyNode::callback, this, _1)

它表示把 MyNode::callback 繫結到當前這個 MyNode 物件上,再把呼叫時的第一個引數傳給 callback

需要注意,this 本質上是一個指標。繫結後的回撥如果活得比當前物件更久,物件銷燬後再呼叫回撥,就可能訪問已經失效的 this,因此非同步回撥中仍然要注意物件生命週期。

4.關鍵語法解釋

寫法 說明 注意事項
_1 呼叫新函式時傳入的第一個引數 來自 std::placeholders
_2 呼叫新函式時傳入的第二個引數 可以用來調整順序
固定值 直接寫在 bind 引數裡 預設會被複製儲存
&Class::method 成員函式指標 後面還要繫結物件
this 成員函式內部的當前物件指標 常作為繫結成員函式時的第二個引數
std::ref(x) 按引用繫結 x 不寫 std::ref 通常會複製

5.bind 和 Lambda 怎麼選

場景 推薦
簡單固定引數 Lambda 或 bind 都可以
引數關係需要讀得很清楚 Lambda
繫結成員函式給舊介面 bind 很常見,也可以用 Lambda
需要複雜邏輯、條件判斷 Lambda
閱讀舊程式碼或 ROS2/Boost.Asio 示例 必須看懂 bind

一句話:新程式碼優先考慮 Lambda;遇到已有介面適配、舊專案程式碼、成員函式回撥時,要能熟練讀懂 std::bind

6.常見錯誤

  1. 忘記佔位符來自 std::placeholders
  2. _1_2 的順序看反,導致實參順序錯誤。
  3. 繫結成員函式時忘記繫結物件。
  4. 繫結物件指標後,物件先被銷燬,回撥還在使用。
  5. 以為 bind 預設按引用儲存引數。預設是複製,需要引用時用 std::ref

7.使用建議

  1. 新程式碼中,引數適配優先嚐試 Lambda。
  2. 讀 ROS2、Boost.Asio、舊專案程式碼時,要能看懂 std::bind(&Class::method, this, _1, _2)
  3. 繫結成員函式時最先考慮物件生命週期。
  4. 需要引用語義時明確寫 std::refstd::cref
  5. 不要為了使用 bind 寫出難讀的佔位符組合,清晰比炫技重要。

8.小結

  • std::bind 用來固定引數、調整引數順序、繫結成員函式。
  • _1_2 表示呼叫新可呼叫物件時傳入的引數位置。
  • 成員函式中可以把 this 作為第二個引數,表示把成員函式繫結到當前物件。
  • bind 預設複製繫結引數,需要引用時用 std::ref
  • 現代 C++ 中很多 bind 用法可以用 Lambda 替代。
  • 理解 bind 對閱讀 ROS2、Boost.Asio 和舊 C++ 專案很有幫助。