std::function与std::bind:C++中的函数包装与绑定

一、引言

在C++编程中,函数的调用和处理通常涉及到多种不同的技术,如函数指针、函数对象(也称为仿函数或functor)、lambda表达式、bind 创建的对象以及重载了函数调用运算符的类。然而,这些技术都有其局限性,例如,每个lambda有它自己唯一的(未命名)类类型;函数及函数指针的类型则由其返回值类型和实参类型决定,等等 。因此,C++11提出std::functionstd::bind 特性。

std::function是一个通用的、多态的函数包装器,它可以保存、复制和调用任何Callable目标——函数、lambda表达式、bind表达式或其他函数对象。这使得我们能够以统一的方式处理各种可调用对象,无需关心它们的具体类型或签名。通过std::function,我们可以将函数作为参数传递给其他函数,或者将函数存储在容器中,实现更为灵活和模块化的代码设计。

std::bind则是一种生成可调用对象的方式,它可以将一个可调用对象与其参数绑定在一起,生成一个新的可调用对象。这个新的可调用对象在调用时会将绑定的参数传递给原始的可调用对象


二、std::function基础

1、std::function的定义与声明

std::function是C++标准库中的一个重要组件,std::function在头文件<functional>。用于封装任何可调用的目标(如函数、lambda表达式、函数对象等),并提供统一的调用语法。std::function 的类型仅取决于其调用签名,而不取决于封装的具体可调用目标的类型。这使得 std::function 非常适合作为回调机制、策略模式等场景中的通用接口。

1.std::function的定义与声明

std::function 的模板参数通常是一个函数签名,即返回类型后跟一个参数包。例如,std::function<int(int, int)> 可以封装任何接受两个 int 参数并返回 int 的可调用目标。声明方式如下:

std::function<返回类型(参数类型)> func;

这里,返回类型表示封装的可调用对象的返回类型,而参数类型则表示其参数类型。你可以根据实际需要替换这些类型。例如,如果要封装一个接受两个整数参数并返回整数的函数,可以这样声明:

std::function<int(int, int)> func;

2. std::function的模板参数

std::function是一个模板类,其模板参数是一个可调用对象的签名,即它的返回类型和参数类型。

template<class Ret, class... Args>
class function<Ret(Args...)>;

Ret是被调用的函数的返回类型,Args…是被调用的函数的形参。

这个签名用于指定std::function对象能够存储和调用的可调用对象的类型。例如,上述的std::function<int(int, int)>就表示它可以存储一个接受两个int参数并返回int的可调用对象。

template <class T> function; 

3. std::function的构造函数与赋值

std::function有多个构造函数和赋值操作符,用于初始化或修改其存储的可调用对象。以下是一些常见的操作形式:

  • 默认构造函数:创建一个空的std::function对象,不存储任何可调用对象。

    std::function<int(int, int)> func; // 默认构造的空的std::function对象
    
  • 通过可调用对象构造:用给定的可调用对象初始化std::function

    void myFunc(int a, int b) { /* ... */ }
    std::function<void(int, int)> func1 = myFunc; // 存储函数指针
    
    // 或者对于lambda表达式
    auto lambda= [](int a, int b) { /* ... */ };
    std::function<void(int, int)> func2 = lambda; // 存储lambda表达式
    
  • 赋值操作:将一个可调用对象赋值给已存在的std::function对象。

    func = myFunc; // 将函数指针赋值给std::function
    func = lambda; // 将lambda表达式赋值给std::function
    
  • 移动赋值操作:将一个std::function对象的内容移动到另一个std::function对象,原对象变为空。

    std::function<void(int, int)> func1 = myFunc;
    std::function<void(int, int)> func2;
    func2 = std::move(func1); // func2现在存储了myFunc,而func1变为空
    

通过这些构造函数和赋值操作,std::function能够灵活地处理各种可调用对象,使得在C++中处理函数和回调函数变得更加方便和统一。

2、std::function的基本使用

std::function 对象内部存储了一个指向封装的可调用目标的指针(或类似机制)。这个可调用目标的类型在实例化 std::function 时不需要指定,只需要知道其调用签名即可。

std::function 对象可以像普通函数那样调用,当调用时,它会转发调用到其封装的目标上。如果 std::function 对象没有封装任何目标(即它是空的),那么尝试调用它将抛出一个 std::bad_function_call 异常。

std::function 还提供了一些成员函数来检查其是否包含有效的可调用目标(例如,通过转换为 bool)、获取封装的目标的类型信息、交换两个 std::function 对象封装的目标等。

非成员函数重载包括关系运算符,用于比较 std::function 对象和空指针(这通常用于检查 std::function 对象是否为空),以及一个 swap 函数,用于交换两个 std::function 对象封装的目标。

  • 包装普通函数

假设我们有一个普通的函数,我们可以使用 std::function 来包装它:

#include <iostream>  
#include <functional>  
  
int add(int a, int b) {  
    return a + b;  
}  
  
int main() {  
    std::function<int(int, int)> func = add;  
    int result = func(2, 3);  
    std::cout << "Result: " << result << std::endl; // 输出:Result: 5  
    return 0;  
}
  • 包装lambda表达式

lambda表达式在 C++11 中被引入,它提供了一种简洁的方式来定义匿名函数对象。我们可以很容易地使用 std::function 来包装 lambda表达式:

#include <iostream>  
#include <functional>  
#include <vector>  
#include <algorithm>  
  
int main() {  
    std::vector<int> numbers = {1, 2, 3, 4, 5};  
    std::function<bool(int)> isEven = [](int n) { return n % 2 == 0; };  
      
    // 使用算法 std::remove_if 移除偶数  
    numbers.erase(std::remove_if(numbers.begin(), numbers.end(), isEven), numbers.end());  
      
    // 输出剩下的奇数  
    for (int num : numbers) {  
        std::cout << num << ' ';  
    } // 输出:1 3 5  
    return 0;  
}
  • 包装函数对象

函数对象(也称为仿函数)是重载了调用运算符 operator() 的类。我们可以使用 std::function 来包装它们:

#include <iostream>  
#include <functional>  
#include <vector>  
#include <algorithm>  
  
class IsEven {  
public:  
    bool operator()(int n) const {  
        return n % 2 == 0;  
    }  
};  
  
int main() {  
    std::vector<int> numbers = {1, 2, 3, 4, 5};  
    std::function<bool(int)> isEvenFunc = IsEven();  
      
    // 使用算法 std::remove_if 移除偶数  
    numbers.erase(std::remove_if(numbers.begin(), numbers.end(), isEvenFunc), numbers.end());  
      
    // 输出剩下的奇数  
    for (int num : numbers) {  
        std::cout << num << ' ';  
    } // 输出:1 3 5  
    return 0;  
}

在上面的例子中,我们定义了一个名为 IsEven 的函数对象,并创建了一个 std::function 对象 isEvenFunc 来包装它。然后,我们像之前一样使用 std::remove_if 算法来移除偶数。

  • 包装类的静态成员函数

静态成员函数与类关联,但不需要类的实例就可以调用。因此,包装静态成员函数相对简单。

#include <iostream>  
#include <functional>  
  
class MyClass {  
public:  
    static void StaticFunc() {  
        std::cout << "Static function called." << std::endl;  
    }  
};  
  
int main() {  
    // 包装静态成员函数  
    std::function<void()> func = &MyClass::StaticFunc;  
    func(); // 调用静态函数  
    return 0;  
}
  • 包装类的成员函数

成员函数与类关联,但需要类的实例才可以调用。

#include <iostream>  
#include <functional>  

class MyClass {
public:
    void memberFunction() {
        std::cout << "Member function called." << std::endl;
    }
};

int main() {
    // 包装非静态成员函数  
    // 非静态成员函数需要对象的指针或者对象去进行调用
    MyClass myClass;
    function<void(MyClass*)> func1 = &MyClass::memberFunction;
    func1(&myClass);
    function<void(MyClass) > func2 = &MyClass::memberFunction;
    func2(myClass);
    return 0;
}
  • 空的std::function对象

    尝试调用一个空的 std::function 对象是未定义行为,通常会导致程序崩溃。因此,在调用 std::function 对象之前,你应该检查它是否为空。

#include <iostream>  
#include <functional>  
  
void print_hello() {  
    std::cout << "Hello, world!" << std::endl;  
}  
  
int main() {  
    std::function<void()> func; // 默认构造一个空的 std::function 对象  

    if (!func)  
        std::cout << "func is empty." << std::endl;  

    func = print_hello; // 赋予一个可调用的目标  
    if (func)  
        func(); // 调用 func,输出 "Hello, world!"  

    func = nullptr; // 重新设置为空  
    if (!func) 
        std::cout << "func is empty again." << std::endl;  

    // 以下代码会导致未定义行为,因为 func 是空的  
    // func();  
  
    return 0;  
}

std::function 的灵活性使得我们可以很容易地将不同的可调用对象作为参数传递给函数,从而编写出更加通用和可重用的代码。不过,需要注意的是,std::function 相比原生的函数指针或 lambda表达式,通常会有更大的开销,因为它需要动态地分配和存储调用的目标。因此,在性能敏感的场合中,需要谨慎使用。


三、std::bind基础

std::bind 是 C++ 标准库 <functional> 头文件中的一个功能,用于将可调用对象(如函数、成员函数、函数对象或 lambda表达式)与一组参数绑定在一起,生成一个新的可调用对象。这个新的对象在调用时会将之前绑定的参数传递给原始的函数或可调用对象。

1、std::bind的定义与声明

1. std::bind的模板参数

std::bind 有两种形式:

  1. 不指定返回类型的版本:
template <class Fn, class... Args>  
bind(Fn&& fn, Args&&... args);
  1. 指定返回类型的版本:
template <class Ret, class Fn, class... Args>  
bind(Fn&& fn, Args&&... args);

这里,Fn 是被绑定的可调用对象(函数、函数对象、lambda表达式等)的类型,Args... 是可变模板参数,表示传递给 fn 的参数。**返回值是函数对象,不是一个函数指针。**即bind函数会返回一个新的可调用对象,该对象在调用时会将参数转发给原始的可调用对象,并可能预先绑定一些参数。

在第一种形式中,返回类型由 fn 的返回类型决定。在第二种形式中,返回类型由模板参数 Ret 明确指定。如果指定了返回类型,它必须是唯一的、不能通过传递给 std::bind 的参数隐式推导出来的模板参数。

std::bind 的参数可以是值或占位符:

  • 如果绑定到值,那么调用返回的函数对象时总是使用这个值作为参数。
  • 如果使用占位符(如 std::placeholders::_1std::placeholders::_2 等),那么调用返回的函数对象时会将对应位置的参数转发给原始函数。

可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对 象来“适应”原对象的参数列表。调用bind的一般形式:

auto newCallable = bind(callable,arg_list);

其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的 callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中 的参数。 arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示 newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对 象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。

即:使用 std::bind 时,你可以指定哪些参数是预先绑定的,哪些参数是占位符(使用 std::placeholders::_N,其中 N 是一个整数,表示参数的位置)。占位符在后续调用新生成的可调用对象时会被实际参数替换。

#include <iostream>  
#include <functional>  
  
void foo(int a, int b, int c) {  
    std::cout << a << ", " << b << ", " << c << std::endl;  
}  
  
int main() {  
    // 使用 std::bind 绑定 foo 函数的第一个参数为 10,第二个参数为 20  
    // 第三个参数使用占位符 _1,意味着调用时提供的第一个参数将填充到这里  
    auto bound_foo = std::bind(foo, 10, 20, std::placeholders::_1);  
  
    // 调用 bound_foo,传递第三个参数 30  
    bound_foo(30); // 输出: 10, 20, 30  
  
    return 0;  
}

在上面的代码中,bound_foo 是一个新的可调用对象,它是通过 std::bind 创建的,将 foo 函数的第一个和第二个参数分别绑定为 1020,而第三个参数则留空,等待后续调用时提供。当我们调用 bound_foo(30) 时,实际上调用的是 foo(10, 20, 30)

2. std::bind的赋值

std::bind本身并不是一个类,因此它没有构造函数。相反,它是一个函数模板,用于生成一个新的可调用对象。这个新的可调用对象是通过调用 std::bind 函数来“构造”的,而不是通过传统意义上的构造函数。

关于赋值,std::bind 返回的可调用对象可以被赋值给一个函数指针类型兼容的可调用对象变量,或者赋值给任何接受这种类型参数的函数或算法。赋值操作与普通对象的赋值类似,但需要注意的是,由于 std::bind 返回的可调用对象通常是复杂且内部状态丰富的,因此赋值可能会导致某些非直观的行为,特别是当涉及到状态共享或移动语义时。

auto bound_foo1 = std::bind(foo, 1, std::placeholders::_1);  
auto bound_foo2 = bound_foo1; // 赋值操作  
bound_foo2(42); // 调用 bound_foo2,等同于调用 foo(1, 42)

在这个例子中,bound_foo1 被赋值给了 bound_foo2,之后 bound_foo2 可以像 bound_foo1 一样被调用。但是需要注意的是,这里的赋值是浅复制,也就是说 bound_foo1bound_foo2 指向的是相同的底层函数对象和数据结构。因此,在特定情况下(例如涉及到修改内部状态的操作时),需要谨慎处理这种赋值操作。

2、std::bind的基本使用

std::bind 的基本使用涵盖了绑定普通函数、成员函数以及带有占位符的参数。占位符 std::placeholders::_N 允许你在绑定时指定哪些参数应该在后续调用时提供。

  • 绑定普通函数

当绑定普通函数时,你可以直接传递函数名和想要预先绑定的参数。

void print_sum(int a, int b) {
    std::cout << a - b << std::endl;
}

int main() {
    // 绑定 print_sum 函数的第二个参数为 5
    auto bound_print_sum = std::bind(print_sum, std::placeholders::_1, 5);

    // 调用 bound_print_sum,传入第一个参数 3
    bound_print_sum(3); // 输出: -2

    return 0;
}

在这个例子中,std::bind 创建了一个新的可调用对象 bound_print_sum,它将 print_sum 函数的第二个参数绑定为 5,第一个参数则留空,等待后续调用时提供。

  • 绑定类的成员函数

当绑定成员函数时,你需要提供对象的指针或引用作为第一个参数(对于非静态成员函数),然后是想要预先绑定的参数。

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

class MyClass {
public:
    void print_with_prefix(const std::string& prefix, const std::string& message) {
        std::cout << prefix << ": " << message << std::endl;
    }
};

int main() {
    MyClass obj;

    // 绑定 obj 的 print_with_prefix 成员函数,并预先绑定 prefix 参数为 "Info"
    auto bound_member_func = std::bind(&MyClass::print_with_prefix, &obj, "Info", std::placeholders::_1);

    // 调用 bound_member_func,传入剩余的 message 参数
    bound_member_func("Hello, world!"); // 输出: Info: Hello, world!

    return 0;
}

在这个例子中,std::bind 创建了一个可调用对象 bound_member_func,它将 MyClassprint_with_prefix 成员函数的 obj 对象和 prefix 参数绑定为 "Info"message 参数则留空等待后续调用时提供。

  • 绑定类的静态成员函数

绑定静态成员函数与绑定普通函数类似,因为静态成员函数不需要类的实例即可调用。当你绑定静态成员函数时,你不需要提供对象实例的指针或引用,只需要提供函数本身和想要预先绑定的参数。

下面是一个绑定静态成员函数的例子:

#include <iostream>
#include <functional>

class MyClass {
public:
    static void staticPrint(int a, int b) {
        std::cout << "Static function called with: " << a << ", " << b << std::endl;
    }
};

int main() {
    // 绑定 MyClass 的静态成员函数 staticPrint,并预先绑定第一个参数为 5
    auto bound_static_func = std::bind(MyClass::staticPrint, 5, std::placeholders::_1);

    // 调用 bound_static_func,传入第二个参数 10
    bound_static_func(10); // 输出: Static function called with: 5, 10

    return 0;
}

在这个例子中,我们创建了一个 MyClass 类,其中包含一个静态成员函数 staticPrint。我们使用 std::bind 来绑定这个静态成员函数,并将第一个参数预先绑定为 5。由于 staticPrint 是一个静态成员函数,我们不需要提供类的实例来调用它。

然后,我们调用 bound_static_func 并传入剩余的第二个参数 10。最终,MyClass::staticPrint 函数被调用,并输出预期的结果。

请注意,因为静态成员函数不依赖于类的实例,所以在 std::bind 的表达式中不需要包含对象指针或引用。这使得绑定静态成员函数比普通成员函数更简单和直接。


四、std::function通常与std::bind结合使用

std::functionstd::bind 可以结合起来使用,以实现更为灵活和可复用的函数调用。std::function 是一个通用、多态的函数封装器,它可以存储、复制和调用任何可调用的目标——函数、lambda表达式、函数对象或其他函数指针。而 std::bind 用于绑定函数或成员函数到一个特定的对象实例,并可能预先绑定一些参数。

std::functionstd::bind 结合使用时,你可以将 std::bind 生成的函数对象存储在 std::function 中,并通过 std::function 来调用它。这种组合允许你创建更加通用和可配置的回调函数或函数对象。

下面是一个 std::functionstd::bind 结合使用的例子:

#include <iostream>
#include <functional>

// 一个简单的函数
void simpleFunction(int x, int y) {
    std::cout << "Function called with: " << x << ", " << y << std::endl;
}

class MyClass {
public:
    // 一个成员函数
    void memberFunction(int a) {
        std::cout << "Member function called with: " << a << std::endl;
    }
};

int main() {
    // 创建一个 MyClass 的实例
    MyClass obj;

    // 使用 std::bind 绑定成员函数到 MyClass 的实例,并预先绑定一个参数
    auto boundMemberFunction = std::bind(&MyClass::memberFunction, &obj, 10);

    // 使用 std::function 来存储绑定的函数对象
    std::function<void()> functionObj = boundMemberFunction;

    // 调用 functionObj,它实际上会调用 MyClass 的 memberFunction
    functionObj(); // 输出: Member function called with: 10

    // 使用 std::bind 绑定普通函数,并预先绑定一个参数
    auto boundSimpleFunction = std::bind(simpleFunction, 1, std::placeholders::_1);

    // 将绑定的普通函数存储在另一个 std::function 对象中
    std::function<void(int)> anotherFunctionObj = boundSimpleFunction;

    // 调用 anotherFunctionObj,它实际上会调用 simpleFunction
    anotherFunctionObj(2); // 输出: Function called with: 1, 2

    return 0;
}

在这个例子中,我们首先创建了一个 MyClass 的实例 obj,然后使用 std::bindMyClass 的成员函数 memberFunction 绑定到 obj 的实例上,并预先绑定了第一个参数为 10。接着,我们将这个绑定的函数对象存储在 std::function<void()> 类型的 functionObj 中。之后,通过调用 functionObj(),我们实际上调用了 objmemberFunction 成员函数。

同样地,我们也展示了如何绑定普通函数 simpleFunction 并将其存储在 std::function<void(int)> 类型的 anotherFunctionObj 中。通过调用 anotherFunctionObj(2),我们实际上调用了 simpleFunction 并传递了参数。

这种结合使用 std::functionstd::bind 的方式提供了极大的灵活性,允许你在运行时动态地改变函数的行为,或者将函数作为参数传递给其他函数或类。然而,正如之前提到的,lambda表达式在现代 C++ 编程中通常更加常用,因为它们提供了更为简洁和直观的语法来创建匿名函数对象。


  • 20
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无敌岩雀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值