【C++ 包装器类 std::function 和 函数适配器 std::bind】 C++11 全面的std::function和std::bind的入门使用教程

简介: 【C++ 包装器类 std::function 和 函数适配器 std::bind】 C++11 全面的std::function和std::bind的入门使用教程

概述

C++11中的std::functionstd::bind是函数对象的重要组成部分,它们可以用于将函数和参数绑定在一起,形成一个可调用的对象。
std::function可以存储任意可调用对象,包括函数指针、函数对象、lambda表达式等,而std::bind则可以将函数和参数绑定在一起,形成一个新的可调用对象。它们的使用可以大大简化代码,提高代码的可读性和可维护性。


可调用对象

C++中有如下几种可调用对象,函数、函数指针、lambda表达式、bind对象、函数对象
其中,lambda表达式和bind对象是C++11标准中提出的(bind机制并不是新标准中首次提出,而是对旧版本中bind1st和bind2st的合并)。


std::function

std::function是一个可调用对象包装器,是一个类模板,可以容纳除了类成员函数指针之外的所有可调用对象,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟它们的执行。
使用std::function可以实现回调函数、事件处理等功能。


std::function函数原型

#include <functional>
template<class R, class... Args>
class function<R(Args...)>;
//其中,R 表示返回值类型,Args... 表示参数类型列表。
//例如,function<int(float, double)> 表示一个返回值为 int,接受一个 float 和一个 double 类型参数的函数对象。

std::function的主要作用

  • 对C++中各种可调用实体(普通函数、Lambda表达式、函数指针、以及其它函数对象等)的封装,形成一个新的可调用的std::function对象,简化调用;
  • 对C++中现有的可调用实体的一种类型安全的包裹(如:函数指针这类可调用实体,是类型不安全的)。

  • 将函数作为参数传递给其他函数;
  • 将函数作为返回值返回;
  • 将函数对象作为参数传递给其他函数;
  • 将函数对象作为返回值返回。

//例如,定义一个返回值为int,参数为两个int的函数对象: 
std::function<int(int, int)>func; 
//可以将一个函数指针或lambda表达式赋值给函数对象: 
int add(int a, int b) { return a +b; } 
func = add; // 函数指针赋值 
func = [](int a, int b) { return a + b; };// lambda表达式赋值 
//调用函数对象可以使用operator(),例如:
int result = func(1, 2); // 调用add函数,返回3

std::function的实现机制

std::function是一个函数对象类,它可以包装任何可调用的对象(函数、函数指针、成员函数、lambda表达式等)并提供一组统一的接口来调用这些可调用对象。std::function的实现机制基于类型擦除技术,它将可调用对象的类型擦除为一个通用的、类型无关的函数指针,并将这个函数指针和一个函数指针调用的包装器(wrapper)存储在一个对象中。
具体地,std::function的实现包括以下步骤:
定义一个模板类std::function,它包含两个成员:一个指向函数包装器的函数指针和一个指向可调用对象的指针(void*类型)。
在std::function的构造函数中,将可调用对象的类型擦除为一个通用的、类型无关的函数指针,然后将这个函数指针和函数包装器存储在std::function对象中。
在std::function的调用函数operator()中,通过函数包装器将函数指针转换为正确的函数类型,并调用可调用对象。


std::function和函数指针的关系

  • 相同点
    std::function和函数指针都可以保存函数的入口地址。
    两者都可以作为参数传递给其他函数。
  • 不同点
    std::function可以保存任何可调用对象(函数指针、仿函数、Lambda表达式等),而函数指针只能保存函数指针类型。
    std::function可以保存带有状态的可调用对象,而函数指针则不行。
    std::function可以自动进行类型推导,而函数指针需要手动指定函数的类型。
  • 如何互相转换
    将函数指针转换为std::function:可以使用std::function的构造函数,将函数指针作为参数传入即可。
    不过通常不建议进行转换,因为必须知道具体类型否则就会出错,不利于程序设计多态的实现,可以用lamda表达式来间接引用。
void foo(int x) {
   std::cout << x << std::endl;
}
std::function<void(int)> func = foo; // 将函数指针转换为std::function

将std::function转换为函数指针:可以使用std::function的target函数,将std::function对象转换为函数指针类型。

void foo(int x) {
   std::cout << x << std::endl;
}
std::function<void(int)> func = foo;
void (*func_ptr)(int) = func.target<void(*)(int)>();

需要注意的是,转换过程中需要保证类型匹配,否则会出现编译错误或者运行期错误。

std::function的target方法

std::function的target方法的目的是获取存储在std::function对象中的底层可调用对象的指针。然而,它只有在底层对象的类型与target方法的模板参数类型完全匹配时才能返回一个有效的指针,否则将返回nullptr。这意味着你需要知道底层对象的确切类型,这对于多态的信号处理程序并不现实。
C++成员函数指针或其他可调用对象。由于C++成员函数和其他可调用对象在内部实现上与普通函数有所不同,因此它们不能直接用作函数指针传参。

std::function的优缺点

  • 优点:

可以方便地实现回调函数、事件处理等功能,同时也可以用于实现函数对象的封装和传递。

  • 缺点:

它的使用会带来一定的性能开销,因为它需要在运行时进行类型检查和动态分配内存。
此外,如果使用不当,也容易引起内存泄漏和对象生命周期管理的问题。

std::function和模板用作参数的对比

模板参数和 std::function 都可以用来传递函数或函数对象作为参数。它们之间的区别在于,模板参数可以接受任意类型的函数指针或函数对象,而 std::function 则是一种类型安全的函数对象封装。
由于模板参数可以接受任意类型的函数指针或函数对象,因此在某些情况下可能会比 std::function 更高效,因为编译器可以直接将函数指针或函数对象作为实参传递给函数,而不需要进行额外的类型检查和转换。另外,使用模板参数还可以避免 std::function 的一些额外开销,如分配内存等。
然而,std::function 也有其优势,它提供了类型安全的函数对象封装,可以避免一些潜在的类型错误。此外,std::function 还支持多态,可以将不同的函数指针或函数对象封装为同一种类型的 std::function 对象,这在某些情况下非常有用。
综上所述,使用模板参数和 std::function 都有其优缺点,具体使用哪种方式取决于具体的情况和需求。


std::bind

std::function是一个可调用对象包装器,是一个类模板,可以容纳除了类成员函数指针之外的所有可调用对象,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟它们的执行。


std::bind函数原型

template<class F, class... Args> 
/unspecified/ bind(F&& f, Args&&... args);
//其中,F是要绑定的函数对象,Args是要绑定的参数。返回值是一个新的可调用对象,可以直接调用或者存储起来后再调用。

std::bind的主要作用

  • 将可调用对象和其参数绑定成一个仿函数;
  • 只绑定部分参数,减少可调用对象传入的参数。

  • 绑定函数对象的参数,生成一个新的可调用对象,可以方便地将函数对象作为参数传递给其它函数。
  • 可以将成员函数绑定到对象上,生成一个新的可调用对象,方便地调用成员函数。
  • 可以将成员函数绑定到对象指针上,生成一个新的可调用对象,方便地调用成员函数。
  • 可以将成员函数绑定到对象引用上,生成一个新的可调用对象,方便地调用成员函数。
  • 可以将函数对象绑定到函数指针上,生成一个新的可调用对象,方便地调用函数对象。
  • 可以将函数对象绑定到函数引用上,生成一个新的可调用对象,方便地调用函数对象。
  • 可以将函数对象绑定到std::function对象上,生成一个新的可调用对象,方便地调用函数对象。

例如,我们有一个函数对象:
void foo(int a, int b, int c) { std::cout << a << " " << b << " " << c << std::endl; }
我们可以使用std::bind将它绑定到一些参数上:
auto f = std::bind(foo, 1, 2, 3);
这里,f是一个新的可调用对象,它绑定了foo函数和参数1、2、3。我们可以像调用原始函数对象一样调用它:
f(); // 输出:1 2 3
我们也可以只绑定部分参数:
auto g = std::bind(foo, 1, std::placeholders::_1, 3);
这里,std::placeholders::_1表示占位符,它表示在调用g时,第二个参数会被传递给foo函数。我们可以这样调用g:
g(2); // 输出:1 2 3
这就是std::bind的基本用法。它可以方便地将函数对象和参数绑定在一起,生成一个新的可调用对象。


注意,如果函数的参数是函数对象类型(如 std::function),则在将函数对象作为参数传递给函数时需要使用占位符来占据函数参数的位置。因为函数对象是一个对象,而不是一个指针或引用,因此需要使用占位符来告诉绑定器函数对象的参数应该在哪里。
而如果函数的参数是函数指针类型,则不需要使用占位符,可以直接将函数指针作为参数传递给函数。因为函数指针本身就是一个指向函数的指针,不需要使用占位符来占据函数参数的位置。


std::bind的实现机制

std::bind是一个函数适配器,它可以将一个可调用对象转换为另一个可调用对象,并可以绑定部分参数。std::bind的实现机制基于模板和可变参数模板技术,它将被绑定的参数和调用时的参数打包为一个元组,并通过std::apply函数展开元组并调用可调用对象。
具体地,std::bind的实现包括以下步骤:
定义一个模板函数std::bind,它接受一个可调用对象和若干个参数,并返回一个绑定了部分参数的可调用对象。
在std::bind中,将被绑定的参数和调用时的参数打包为一个元组。
在std::bind返回的可调用对象中,通过std::apply函数展开元组并调用可调用对象。


std::bind的优缺点

  • 优点:

可以方便地实现函数对象的复用和参数的延迟绑定,从而提高代码的可读性和可维护性。

  • 缺点:

可能会导致代码的复杂性增加,特别是当参数较多时,需要谨慎使用。


注意点

  • 预绑定的参数是以值传递的形式,不预绑定的参数要用std::placeholders(占位符)的形式占位,从_1开始,依次递增,是以引用传递的形式;
  • std::placeholders表示新的可调用对象的第几个参数,而且与原函数的该占位符所在位置的进行匹配;
  • bind绑定类成员函数时,第一个参数表示对象的成员函数的指针,第二个参数表示对象的地址,这是因为对象的成员函数需要有this指针。并且编译器不会将对象的成员函数隐式转换成函数指针,需要通过&手动转换;
  • std::bind的返回值是可调用实体,可以直接赋给std::function

代码示例

#pragma once
#include <iostream>
#include <functional>
class A {
public:
 bool TESTA(int, char*, int) { /* implementation */ }
};
class B {
public:
 bool TESTB(std::function<bool(int, char*, int)> func) { /* implementation */ }
};
int main() {
 A objA;
 B objB;
 auto lambda = [](int a, char* b, int c) { /* implementation */ };
 objB.TESTB(lambda);
 objB.TESTB(std::bind(&A::TESTA, &objA, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
 return 0;
}

总结

std::function和std::bind都是C++11中非常有用的函数对象类,它们的实现机制都是基于模板和类型擦除技术。
std::function通过类型擦除将可调用对象的类型擦除为一个通用的、类型无关的函数指针,并通过函数包装器调用可调用对象;
std::bind通过可变参数模板和std::apply函数将被绑定的参数和调用时的参数打包为一个元组,并调用可调用对象。

目录
相关文章
|
1月前
|
设计模式 安全 数据库连接
【C++11】包装器:深入解析与实现技巧
本文深入探讨了C++中包装器的定义、实现方式及其应用。包装器通过封装底层细节,提供更简洁、易用的接口,常用于资源管理、接口封装和类型安全。文章详细介绍了使用RAII、智能指针、模板等技术实现包装器的方法,并通过多个案例分析展示了其在实际开发中的应用。最后,讨论了性能优化策略,帮助开发者编写高效、可靠的C++代码。
39 2
|
2月前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
2月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
88 6
|
2月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
46 0
|
2月前
|
存储 安全 编译器
【C++打怪之路Lv1】-- 入门二级
【C++打怪之路Lv1】-- 入门二级
32 0
|
2月前
|
自然语言处理 编译器 C语言
【C++打怪之路Lv1】-- C++开篇(入门)
【C++打怪之路Lv1】-- C++开篇(入门)
39 0
|
2月前
|
分布式计算 Java 编译器
【C++入门(下)】—— 我与C++的不解之缘(二)
【C++入门(下)】—— 我与C++的不解之缘(二)
|
2月前
|
编译器 Linux C语言
【C++入门(上)】—— 我与C++的不解之缘(一)
【C++入门(上)】—— 我与C++的不解之缘(一)
|
1月前
|
中间件 Docker Python
【Azure Function】FTP上传了Python Function文件后,无法在门户页面加载函数的问题
通过FTP上传Python Function至Azure云后,出现函数列表无法加载的问题。经排查,发现是由于`requirements.txt`中的依赖包未被正确安装。解决方法为:在本地安装依赖包到`.python_packages/lib/site-packages`目录,再将该目录内容上传至云上的`wwwroot`目录,并重启应用。最终成功加载函数列表。
|
2月前
|
JavaScript
箭头函数与普通函数(function)的区别
箭头函数是ES6引入的新特性,与传统函数相比,它有更简洁的语法,且没有自己的this、arguments、super或new.target绑定,而是继承自外层作用域。箭头函数不适用于构造函数,不能使用new关键字调用。

热门文章

最新文章