条款34:优先选用 lambda 式,而非 std bind

ZaynPei Lv6

在 C++11 中,lambda 表达式几乎总是比 std::bind 更好的选择;到了 C++14,std::bind 基本上已无用武之地。std::bind 是一个源自 C++98 时代函数式编程思想的工具,虽然在 C++11 标准库中被正式引入,但其设计与现代 C++ 的 lambda 表达式相比,在多个方面都相形见绌。

std::bind

std::bind 是 C++11 在 头文件中提供的一个非常有用的函数模板。它就像一个函数适配器,可以接受一个可调用对象(callable object),并将其部分或全部参数绑定到特定的值或占位符上,最终生成一个新的、可直接调用的对象(通常称为函数对象 function object)。

简而言之,std::bind 的核心作用是延迟调用和参数定制。

std::bind 的基本语法如下:

1
auto new_callable = std::bind(callable_object, arg1, arg2, ...);
  • callable_object: 任何可以被调用的对象,例如普通函数指针, 类的成员函数指针, Lambda 表达式, 其他函数对象(如 std::function)等
  • arg1, arg2, …: 要传递给 callable_object 的参数列表。这些参数可以是具体的值(如 10, “hello”, 3.14), 这些参数会被复制或移动并存储在生成的新函数对象中; 也可以是占位符 (Placeholders): 如 _1, _2, _3, …。它们定义在命名空间 std::placeholders 中。占位符表示新生成的可调用对象的参数位置。例如,_1 表示新对象的第一个参数,_2 表示第二个参数,以此类推(注意:使用占位符时,通常需要 using namespace std::placeholders; 或显式指定 std::placeholders::_1)

主要用途与代码示例

下面我们通过几个核心场景来理解 std::bind 的具体用法。

  1. 绑定普通函数参数 这是最常见的用法,用于将一个函数的某些参数固定下来,生成一个参数更少的新函数。假设有一个函数需要三个参数,但我们想创建一个只需要一个参数的简化版本。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    #include <iostream>
    #include <functional>

    // 原始函数,接受三个参数
    void print_info(const std::string& name, int age, const std::string& city) {
    std::cout << name << " is " << age << " years old and lives in " << city << "." << std::endl;
    }

    int main() {
    // 使用占位符命名空间
    using namespace std::placeholders;

    // 1. 绑定部分参数
    // 将 print_info 的第一个和第三个参数固定为 "Alice" 和 "New York"
    // _1 是一个占位符,代表新函数 call_alice 的第一个参数
    // 第二个参数被设置为占位符 _1,这意味着 call_alice 的第一个参数将会被传递到 print_info 的第二个位置。
    auto call_alice = std::bind(print_info, "Alice", _1, "New York");

    // 调用新生成的函数对象,只需要提供年龄
    std::cout << "--- Calling with partial binding ---" << std::endl;
    call_alice(30); // 输出: Alice is 30 years old and lives in New York.
    call_alice(25); // 输出: Alice is 25 years old and lives in New York.


    // 2. 绑定所有参数
    // 将所有参数都固定下来
    auto call_bob = std::bind(print_info, "Bob", 42, "London");

    // 调用时不再需要任何参数
    std::cout << "\n--- Calling with full binding ---" << std::endl;
    call_bob(); // 输出: Bob is 42 years old and lives in London.

    return 0;
    }
  2. 调整参数顺序: std::bind 还可以通过占位符灵活地调整参数的传递顺序。假如有一个减法函数,我们想创建一个新函数来实现参数顺序颠倒的减法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    #include <iostream>
    #include <functional>

    double subtract(double a, double b) {
    return a - b;
    }

    int main() {
    using namespace std::placeholders;

    // 原始调用
    std::cout << "subtract(10, 3) = " << subtract(10, 3) << std::endl; // 输出: 7

    // 使用 bind 交换参数顺序
    // _1 对应新函数的第一个参数,_2 对应第二个
    // bind(subtract, _2, _1) 的意思是:
    // 调用时,将新函数的第二个参数传给 subtract 的第一个参数
    // 将新函数的第一个参数传给 subtract 的第二个参数
    auto reversed_subtract = std::bind(subtract, _2, _1);

    std::cout << "reversed_subtract(10, 3) = " << reversed_subtract(10, 3) << std::endl; // 输出: -7
    // 上述调用等效于 subtract(3, 10)

    return 0;
    }
  3. 绑定类的成员函数: 这是 std::bind 一个非常重要的应用场景,尤其是在回调函数中。绑定成员函数时,必须提供一个类的实例(或指针、引用)作为 std::bind 的第一个参数(在函数参数之后)。也就是说, 绑定成员函数时,第一个参数必须是成员函数指针,第二个参数必须是对象实例(或指针)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    #include <iostream>
    #include <functional>
    #include <string>

    class Greeter {
    public:
    void say_hello(const std::string& name) {
    std::cout << "Hello, " << name << "!" << std::endl;
    }
    };

    int main() {
    using namespace std::placeholders;
    Greeter greeter_instance;

    // 绑定成员函数
    // 第一个参数是成员函数指针:&Greeter::say_hello
    // 第二个参数是对象实例的地址:&greeter_instance
    // 第三个参数 _1 是占位符,对应 say_hello 的 name 参数
    auto greet_func = std::bind(&Greeter::say_hello, &greeter_instance, _1);

    greet_func("World"); // 输出: Hello, World!
    greet_func("C++"); // 输出: Hello, C++!

    // 如果传递对象实例本身,会发生拷贝
    auto greet_func_copy = std::bind(&Greeter::say_hello, greeter_instance, _1);
    greet_func_copy("Copied"); // 输出: Hello, Copied!

    return 0;
    }
    > &greeter_instance:这是调用该成员函数的对象实例的指针。this 指针被隐式地绑定到了 greeter_instance。你也可以直接传递 greeter_instance,此时会拷贝一份对象。

std::bind 与 Lambda 表达式的对比

在现代 C++ (C++14 及以后) 中,Lambda 表达式通常是比 std::bind 更好的选择。因为 Lambda 通常更具可读性、更灵活,并且可能产生更高效的代码。让我们用 Lambda 重写上面的第一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <string>
#include <functional>

void print_info(const std::string& name, int age, const std::string& city) {
std::cout << name << " is " << age << " years old and lives in " << city << "." << std::endl;
}

int main() {
// ---- std::bind 版本 ----
using namespace std::placeholders;
auto call_alice_bind = std::bind(print_info, "Alice", _1, "New York");
call_alice_bind(30);

// ---- Lambda 版本 ----
// 捕获 name 和 city 变量,age 作为参数传入
std::string name = "Alice";
std::string city = "New York";
auto call_alice_lambda = [name, city](int age) {
print_info(name, age, city);
};
call_alice_lambda(30);

return 0;
}
Lambda 的优势: - 可读性更强:name, city{…} 的意图非常清晰:捕获 name 和 city,并接受一个 age 参数。而 std::bind(…, “Alice”, _1, …) 的 _1 语法相对晦涩。 - 更灵活:Lambda 内部可以包含更复杂的逻辑,定义局部变量等,而 std::bind 只是单纯的函数调用包装。 - 性能可能更好:编译器通常能更好地内联和优化 Lambda 表达式,因为 Lambda 的类型是唯一的、在编译期确定的。而 std::bind 产生的函数对象类型较为复杂,可能给优化带来挑战。 - 无需占位符:Lambda 自然地处理参数,不需要引入 std::placeholders 命名空间和 _1, _2 等符号。

On this page
条款34:优先选用 lambda 式,而非 std bind