C++11 可调用对象体系详解:从 reference_wrapper 到 bind 与 function

目录

​编辑

一、包装器

[1.1 什么是包装器](#1.1 什么是包装器)

[1.2 为什么需要包装器](#1.2 为什么需要包装器)

二、reference_wrapper

[2.1 为什么引用不能放进容器](#2.1 为什么引用不能放进容器)

[2.2 reference_wrapper 的出现](#2.2 reference_wrapper 的出现)

[2.3 ref的使用](#2.3 ref的使用)

三、bind

[3.1 什么是bind](#3.1 什么是bind)

[3.2 占位符](#3.2 占位符)

[3.3 bind 的使用](#3.3 bind 的使用)

[3.4 bind 底层原理](#3.4 bind 底层原理)

[3.5 bind 的缺点](#3.5 bind 的缺点)

四、function

[4.1 什么是function](#4.1 什么是function)

[4.2 function 的使用](#4.2 function 的使用)

一、包装器

1.1 什么是包装器

包装器是对已有对象进行再次封装,使其拥有新的行为或接口。

例如:指针包装成智能指针,函数包装成函数对象,引用包装成对象。

核心思想:原始对象 -> 经过包装器包装 -> 获得新的能力

1.2 为什么需要包装器

举几个经典问题:

问题1

模板无法直接存储引用,vector<int&> 非法,于是出现了 reference_wrapper<int> 。

问题2

不同类型函数无法统一存储

对于普通函数、Lambda、仿函数、成员函数它们的类型完全不同,需要 std::function 统一包装。

问题3

参数顺序固定,func(a, b, c) 想提前绑定部分参数,需要 std::bind 。

二、reference_wrapper

2.1 为什么引用不能放进容器

引用的特点:必须初始化,不可重新绑定。对于 vector<int&> 无法实现,是因为 vector 不是一个个对象尾插的,它底层是2倍或1.5倍扩容机制,这就导致了vector多扩容的部分对象无法初始化。假如不存在扩容机制,只是一个个扩容,那么对于 v.push_back(a); v.back() = c; 这份代码意味着,v 的最后一个元素引用了a对象,但是 v.back() = c; 意味着将 c 的值赋值给了 v 的最后一个元素,不是引用重新绑定了 c 对象。由于vector底层有时扩容会释放旧空间,开辟新空间,这一点也是无法解决的。

2.2 reference_wrapper 的出现

对于上述问题,C++98的解决方案是容器不存引用,而是存指针。

vector<int*> v;

v.push_back(&a);

v.push_back(&b);

这样做虽然可以达到目的,但是使用不方便,如 (*v0)++; 需要解引用。

于是标准库提供了

复制代码
template <class T> class reference_wrapper;

它存在于 <functional> 这个头文件中。

它的本质就是:引用的对象化,把引用包装成一个类,底层原理就是包装指针的类。

示例:

cpp 复制代码
#include <iostream>
#include <functional>
using namespace std;

int main()
{
	int a = 0;
	reference_wrapper<int> ra = a;

	ra.get() = 100;

	cout << a << endl;
	return 0;
}

2.3 ref的使用

在实际开发中几乎不会这样写

reference_wrapper<int> ra = a;

而是

ref(a)

例如:

int a = 10;

auto rw = std::ref(a);

ref 就是一个构造 reference_wrapper对象的函数。

学到这里,就能解决容器中如何存引用的问题。

cpp 复制代码
#include <iostream>
#include <vector>
#include <functional>
using namespace std;

int main()
{
	vector<reference_wrapper<int>> v;
	int a = 10;
	v.push_back(ref(a));
	cout << v.back().get() << endl;

	int b = 20;
	v.back() = b;
	cout << v.back() << endl;

	return 0;
}

对于 reference_wrapper 和 ref 还有很多其他用途,例如解决 thread 和 bind 传引用的问题。

三、bind

3.1 什么是bind

std::bind 是一个函数模板,用于生成可调用对象的包装器(函数适配器)。它接收一个可调用对象,并根据参数绑定规则(值绑定、引用绑定、占位符等)生成一个新的可调用对象。

该新可调用对象可以对原函数的参数进行绑定、重排或部分固定,从而改变函数的调用形式。
基本语法:auto newCallable = bind(callable, arg_list);

其中 newCallable 本身是一个可调用对象,用来接受bind返回的可调用对象,callable 是bind处理的目标对象,arg_list 是参数绑定规则。

3.2 占位符

arg_list 中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是占位符,表示 newCallable的参数,它们占据了传递给 newCallable 的参数位置。数值n表示生成的可调用对象中参数的位置:_1 为newCallable的第一个参数,_2 为第二个参数,以此类推,_n 为第n个参数。

这些占位符存放在了placeholders 的一个命名空间中。

3.3 bind 的使用

cpp 复制代码
// 基本用法:绑定函数 + 固定参数
int add(int a, int b)
{
    return a + b;
}

auto f = std::bind(add, 10, 20);

cout << f();  // 30

这里发生的是:add 函数经过 bind 函数的处理,返回一个新的可调用对象,其中可调用对象的第一个参数被固定为 10,第二个参数被固定为 20,所以 f 就不需要传递参数,也不能传递参数。

cpp 复制代码
// 使用占位符
using namespace std::placeholders;

auto f = std::bind(add, 10, _1);

f(5);

调用 f(5); 等价于add(10, 5);

只需记住一句话,_1 表示:调用时再传进来的第一个参数。其中_1 跟add没有关系,它不是代表add的第一个参数。

cpp 复制代码
// 参数重排
auto f = std::bind(add, _2, _1);

f(10, 20);

调用 f(10, 20); 等价于 add(20, 10);

因为_1 是f(10,20) 的第一个参数,_2 是f(10, 20)的第二个参数。

cpp 复制代码
// 忽略参数
auto f = std::bind(add, _1, 100);

f(5);

调用 f(5); 等价于 add(5, 100);

cpp 复制代码
// 绑定成员函数
struct Test 
{
    int add(int x) 
    {
        return x + 10;
    }
};

Test t;
auto f = std::bind(&Test::add, &t, _1);

调用 f(5); 等价于 t.add(5);

对于成员函数,它们的参数列表会隐含地多一个this指针指向要调用成员函数的对象,所以必须要传一个对象的地址。

cpp 复制代码
// bind + ref
int x = 10;

auto f = std::bind(add, std::ref(x), _1);

作用:对于bind默认生成的可调用对象,都是传值传参,但对上述代码来说,不是拷贝 x 而是引用x,可以修改 x 的值。

3.4 bind 底层原理

bind 返回的到底是什么?

bind 返回的是一个匿名函数对象(仿函数)。

可以理解为它内部生成了类似的类

cpp 复制代码
class BindObject
{
    FunctionType func;     // 原函数
    tuple stored_args;     // 绑定的参数

public:
    template<class... CallArgs>
    auto operator()(CallArgs&&... call_args)
    {
        return invoke(func, stored_args, call_args...);
    }
};

bind的核心结构可以拆成 3 层

第一层:保存可调用对象 (普通函数、lambda、成员函数、函数对象)

第二层:保存参数绑定规则 (值绑定、引用绑定、占位符)

第三层:调用时展开参数 (依次给可调用对象传递保存的参数)

3.5 bind 的缺点

  1. 可读性差

bind(add, _2, _1)

对于不明白的人,跟可能认为 _1 为add的第一个参数,_2 为add的第二个参数,不直观

  1. 错误信息难看

bind为一个函数模板,且返回一个新的可调用对象,这个过程是十分复杂的,出错了不容易排查

  1. lambda 可以完全替代

实际场景中,没有人会用bind来调整函数参数顺序,都是用来固定某个参数,对于lambda也可以很简洁地完成该功能。

cpp 复制代码
auto f = [](int x){
    return add(10, x);
};

四、function

4.1 什么是function

function是一个类模板,也是一个包装器。function实例化出的对象可以包装并存储可调用对象,包括函数对象、仿函数、lambda、bind表达式、成员函数、普通函数等,存储的可调用对象被称为function的目标。若function不含目标,则它为空,调用空function的目标会抛出bad_function_call 异常。

它也被定义在<functional>的头文件中。

它主要的功能是:把不同类型的可调用对象统一成一个类型

基本语法:function<返回值(参数)> f;

4.2 function 的使用

普通函数

cpp 复制代码
int add(int x, int y) { return x + y; }

std::function<int(int, int)> f1 = add;

f1(2,5);

lambda

cpp 复制代码
std::function<int(int, int)> f2 = [](int x, int y) { return x + y; };

f2(3, 4);

仿函数

cpp 复制代码
struct Functor 
{
    int operator()(int x, int y) 
    {
        return x + y;
    }
};

std::function<int(int, int)> f3 = Functor();

f3(1, 3);

bind表达式

cpp 复制代码
std::function<int(int, int)> f4 = std::bind(add, 10, std::placeholders::_1);

f4(2);

最常用的场景之一

对于对某些特定的指令,执行特定函数。

cpp 复制代码
// function作为map的映射可调用对象的类型​
map<string, function<int(int, int)>> opFuncMap = {
{"+", [](int x, int y){return x + y;}},
{"-", [](int x, int y){return x - y;}},
{"*", [](int x, int y){return x * y;}},
{"/", [](int x, int y){return x / y;}}
};
相关推荐
森G2 小时前
77、线程池原理和实现------服务器源码解析----云视频服务项目
服务器·c++·qt
qq3621967052 小时前
阿里裁员新消息(2026最新动态汇总)
java·开发语言·前端
.千余2 小时前
【C++】模板进阶全解:非类型参数|全特化|偏特化|分离编译完全指南
开发语言·c++·笔记·学习·其他
代码改善世界2 小时前
【C++进阶】C++11:列表初始化、右值引用与移动语义、完美转发全解析
java·开发语言·c++
scx_link2 小时前
通过git bash在本地创建分支,并推送到远程仓库中
开发语言·git·bash
GZ同学2 小时前
单双变量Ripley’s K函数 R 语言实现
开发语言·r语言
Channing Lewis2 小时前
PHP 解析 Excel 的那些坑:一次“行号错位”引发的数据丢失
开发语言·php·excel
牛油果子哥q3 小时前
并查集(DSU)超精讲,路径压缩、按秩合并、万能模板、连通性判定、最小生成树与刷题实战全解
数据结构·c++·最小生成树·并查集
小小龙学IT3 小时前
Apache Airflow 2.x 深度指南:用 Python 编排一切的现代化工作流引擎
开发语言·python·apache
少爷晚安。3 小时前
Java基础02_JDK&JRE下载安装及环境配置
java·开发语言