C++11 核心特性解析(二):包装器、参数包与 emplace 进阶技术体系详解

目录

一、引用折叠

[1.1 万能引用](#1.1 万能引用)

[1.2 完美转发](#1.2 完美转发)

二、可变参数模板

[2.1 基本用法](#2.1 基本用法)

[2.2 包扩展](#2.2 包扩展)

2.2.1.递归展开

[2.2.2 非递归展开](#2.2.2 非递归展开)

三、lambda

[3.1 匿名函数与Lambda](#3.1 匿名函数与Lambda)

[3.2 基础语法与使用](#3.2 基础语法与使用)

[3.3 捕获列表](#3.3 捕获列表)

[3.4 lambda的原理](#3.4 lambda的原理)

四、包装器

[4.1 funciton](#4.1 funciton)

[4.1.1 基本语法与使用](#4.1.1 基本语法与使用)

[4.2 bind](#4.2 bind)

[4.2.1 基本语法](#4.2.1 基本语法)

[4.2.2 绑定成员函数](#4.2.2 绑定成员函数)

五、empalce系列接口

[5.1 与传统接口不同点](#5.1 与传统接口不同点)

[5.2 与传统接口相同点](#5.2 与传统接口相同点)


一、引用折叠

1.1 万能引用

C++中不能直接定义引用的引用如 int& && r = i; ,这样写会直接报错,通过模板或typedef 中的类型操作可以构成引用的引用。通过模板或typedef中的类型操作可以构成引用的引用时,这时C++11给出了⼀个引用折叠的规则:

组合形式 折叠结果 说明
T& & T& 左值引用 + 左值引用 → 左值引用
T& && T& 左值引用 + 右值引用 → 左值引用
T&& & T& 右值引用 + 左值引用 → 左值引用
T&& && T&& 右值引用 + 右值引用 → 右值引用

一句话总结只要有一个左值引用 &,结果就是左值引用;只有两个都是右值引用 &&,结果才是右值引用

cpp 复制代码
// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤
template<class T>
void f1(T& x)
{}

// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤
template<class T>
void f2(T&& x)
{}
int main()
{
	typedef int& lref;
	typedef int&& rref;
	int n = 0;
	lref& r1 = n;  // r1 的类型是int&
	lref&& r2 = n; // r2 的类型是int&
	rref& r3 = n;  // r3 的类型是int&
	rref&& r4 = 1; // r4 的类型是int&&
	// 没有折叠->实例化为void f1(int& x)
	f1<int>(n);
	f1<int>(0);  // 报错

	// 折叠->实例化为void f1(int& x)
	f1<int&>(n);
	f1<int&>(0); // 报错

	// 折叠->实例化为void f1(int& x)
	f1<int&&>(n);
	f1<int&&>(0); // 报错// 折叠->实例化为void f1(const int& x)

	f1<const int&>(n);
	f1<const int&>(0);
	f1<const int&&>(n);

	// 折叠->实例化为void f1(const int& x)
	f1<const int&&>(0);
	// 没有折叠->实例化为void f2(int&& x)
	f2<int>(n);   // 报错

	f2<int>(0);// 折叠->实例化为void f2(int& x)
	f2<int&>(n);
	f2<int&>(0);  // 报错

	// 折叠->实例化为void f2(int&& x)
	f2<int&&>(n); // 报错

	f2<int&&>(0);
	return 0;
}

像f2这样的函数模板中,T&&x参数看起来是右值引用参数,但是由于引用折叠的规则,他传递左 值时就是左值引用,传递右值时就是右值引用,有些地放也把这种函数模板的参数叫做万能引用。

cpp 复制代码
template<class T>
void Function(T&& t)
{
	int a = 0;
	T x = a;
	//x++;
	cout << &a << endl;
	cout << &x << endl << endl;
}

int main()
{
	// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
	// 右值
	Function(10);

	// 左值
	int a;
	// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)
	Function(a);

	// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
	Function(std::move(a));

	const int b = 8;
	// b是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int& t)
	// 但是Function内部会编译报错,x不能++
	Function(b);

	//Function内部会编译报错,x不能++
	//const右值
	Function(std::move(b));
	return 0;
}

Function(T&&t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模 板参数T的推导int&,再结合引用折叠规则,就实现了实参是左值,实例化出左值引用版本形参的 Function,实参是右值,实例化出右值引用版本形参的Function。

1.2 完美转发

Function(T&&t)函数模板程序中,传左值实例化以后是左值引用的Function函数,传右值实例化 以后是右值引用的Function函数。但是结合我们前面的讲解,变量表达式都是左值属性,也就意味着一个右值被右值引用绑定 后,右值引用变量表达式的属性是左值,也就是说Function函数中 t 的属性是左值,那么我们把 t 传 递给下一层函数Fun,那么匹配的都是左值引用版本的Fun函数 。这里我们想要保持t对象的属性, 就需要使用完美转发实现**。**

cpp 复制代码
void Fun(int& t)
{
	std::cout << "左值引用" << std::endl;
}
void Fun(int&& t)
{
	std::cout << "右值引用" << std::endl;
}
void Fun(const int& t)
{
	std::cout << "const 左值引用" << std::endl;
}
void Fun(const int&& t)
{
	std::cout << "const 右值引用" << std::endl;
}
template<class T>
void Function(T&& t)
{
	Fun(t);
}

int main()
{
	// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
	Function(10);
	// a是左值,推导T为int&,引⽤折叠,模板实例化为void Function(int& t)
	int a;
	Function(a);

	// move(a)是右值,推导T为int,引⽤折叠,模板实例化为void Function(int&& t)
	Function(std::move(a));

	const int b = 8;
	//b是const 左值,推导T为const int&,引⽤折叠,模板实例化为void Function(const int& t)
	Function(b);

	// move(a)是const 右值,推导T为const int,引⽤折叠,模板实例化为void Function(const int&& t)
	Function(std::move(b));
	return 0;
}

完美转发就是:在模板函数里,把传入的实参的值类别(左值 / 右值)和类型原封不动地转发给下一个函数,既不额外拷贝,也不改变参数性质。

它依赖两个核心技术:

  1. 万能引用(T&&
  2. 引用折叠
  3. std::forward
cpp 复制代码
template<class T>
void Function(T&& t)
{
	//完美转发
	Fun(std::forward<T>(t));
}

std::forward本质就是根据 T 的类型,把 参数强制转回原本的值类别。

  • 如果 T 推导为左值引用 → 返回左值引用
  • 如果 T 推导为非引用 / 右值引用 → 返回右值引用

二、可变参数模板

可变参数模板是 C++11 引入的能接收 任意个数、任意类型参数的模板机制,是现代 C++ 泛型编程的核心,常和完美转发搭配实现万能转发 / 工厂函数。

2.1 基本用法

在我们之前的函数模板中,模板参数是固定的一个或者多个。编译器虽然替我们显式实例化出了函数接口但是比较呆板不够灵活。比如,我们要实现一个Print函数支持多种不同类型参数的打印工作在学习可变参数模板之前我们使用函数模板只能这样实现:

cpp 复制代码
template <class T1>
void Print(T1&& arg1)
{
	std::cout << arg1 << std::endl;
}

template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2)
{
	std::cout << arg1 <<" " << arg2 << std::endl;
}

template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3)
{
	std::cout << arg1 <<" " << arg2 <<" " << arg3 << std::endl;
}


int main()
{
	double x = 2.2;
	Print();
	Print(1);
	Print(1, std::string("xxxxx"));
	Print(1.1, std::string("xxxxx"), x);
}

我们发现,虽然这种方法具有一定可行性但是非常呆板不够灵活,在示例代码中我们只是支持了最多3个不同类型参数的打印如果后续用户要求打印4个或更多呢?按照老方法我们就必须一步步定义对应的函数模板。下面我们就来了解一下可变参数模板以及其给我们带来的便捷性:

C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称 为参数包,存在两种参数包:

  • 模板参数包:typename/class... Args 表示零或多个模板参数;
  • 函数参数包:Args&&... args 表示零或多个函数参数。

我们用...来指出⼀个模板参数或函数参数的表示一个包,在模板参数列表中,class...或 typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出 接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板 一样,每个参数实例化时遵循引用折叠规则。

利用可变参数模板实现前面的我们可以实现出这样的代码:

cpp 复制代码
template<class... Args>
void Print(Args&&... args)
{
	//可以使⽤sizeof...运算符去计算参数包中参数的个数。
	std::cout << "包中有" << sizeof...(args) <<"个参数" << std::endl;
}
int main()
{
	double x = 2.2;
	Print();
	Print(1);
	Print(1, std::string("xxxxx"));
	Print(1.1, std::string("xxxxx"), x);
	Print(1, std::string("xxxxx"), x, 44);
	Print(1, std::string("xxxxx"), x, std::string("yyyyy"));
}

这段代码相比于我们之前实现的Print接口可谓进步了不少,在之前的代码中每个函数模板的模板参数都是固定的,需要打印多少不同类型的参数就必须实现对应的函数模板。但是在这段代码中我们使用参数包每次将多种不同类型的实参整体"封装"成了一个包并传递给形参。在函数内部我们使用sizeof...运算符将包中的参数个数依次统计了出来。

但是现在我们的代码中只是统计了参数包中参数的个数,并没有将其一一解析并打印出来这就要涉及到参数包的解析与扩展了,下面我们就来了解一下包扩展:

2.2 包扩展

包扩展是 C++11 引入的核心语法,用于将参数包 展开为独立的参数序列,是可变参数模板的基础操作,一共有两种方法:

2.2.1.递归展开

cpp 复制代码
void ShowList()
{
	// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数
	std::cout << std::endl;
}

template<class T,class ...Args>
void ShowList(T x, Args... args)
{
	std::cout << x <<" ";
	// args是N个参数的参数包
	// 调⽤ShowList,参数包的第⼀个传给x,剩下N - 1传给第⼆个参数包
	ShowList(args...);
}

// 编译时递归推导解析参数
template<class ...Args>
void Print(Args... args)
{
	ShowList(args...);
}

int main()
{
	double x = 2.2;
	Print(1, std::string("xxxxx"), x);
	return 0;
}

需要注意的是,整个过程是编译时编译器自动推导的,与运行时是两种情况。上面的代码在编译时编译器完成了以下步骤:

当然我们显式的定义出来相关接口也是可以的:

cpp 复制代码
void ShowList()
{
	// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数
	std::cout << std::endl;
}
void ShowList(double x3)
{
	std::cout << x3 << " ";
	ShowList();
}
void ShowList(std::string x2, double x3)
{
	std::cout << x2 << " ";
	ShowList(x3);
}
void ShowList(int x1, std::string x2,double x3)
{
	std::cout << x1 << " ";
	ShowList(x2,x3);
}
void Print(int x1, std::string x2, double x3)
{
	ShowList(x1,x2,x3);
}
int main()
{
	double x = 2.2;
	Print(1, std::string("xxxxx"), x);
	return 0;
}

需要再次强调的是,整个过程都是编译器在编译期间自动推导的。这里我们可以用一个典型的例子来验证一下,我们将递归结束的条件写在接口内部而不是单独定义一个接口,虽然语义上没有问题但是编译器会报错:

cpp 复制代码
template<class T,class ...Args>
void ShowList(T x, Args... args)
{
	//将递归结束条件定义在接口内,编译器报错
	if (sizeof...(args) == 0)
	{
		std::cout << std::endl;
	}

	std::cout << x <<" ";
	// args是N个参数的参数包
	// 调⽤ShowList,参数包的第⼀个传给x,剩下N - 1传给第⼆个参数包
	ShowList(args...);
}

// 编译时递归推导解析参数
template<class ...Args>
void Print(Args... args)
{
	ShowList(args...);
}

2.2.2 非递归展开

cpp 复制代码
template <class T>
const T& GetArg(const T& x)
{
	std::cout << x << " ";
	return x;
}

template <class ...Args>
void Arguments(Args... args)
{}

template <class ...Args>
void Print(Args... args)
{
	// 注意GetArg必须返回或者到的对象,这样才能组成参数包给Arguments
	Arguments(GetArg(args)...);
}
int main()
{
	double x = 2.2;
	Print(1, std::string("xxxxx"), x);
	return 0;
}

GetArg(args)... 是模式包扩展:编译器会将 args 参数包中的每个元素,都代入 GetArg(...) 模板函数,生成一个参数序列。

例如调用 Print(1, 2.2, "abc"),会被展开为:

cpp 复制代码
Arguments(GetArg(1), GetArg(2.2), GetArg("abc"));

这种方式不需要递归,直接一次性展开所有参数。

三、lambda

3.1 匿名函数与Lambda

匿名函数是没有显式命名的函数,它把一段可执行逻辑封装成 "值",可以直接赋值给变量、作为参数传递给其他函数,或作为返回值返回。

  • 无名称:不需要像普通函数那样定义void func(),直接写逻辑体。
  • 可传递:本质是 "函数类型的值",能在代码间流转,是实现高阶函数(接收 / 返回函数的函数)的基础。
  • 上下文捕获:多数语言的匿名函数可以访问定义它的外部作用域变量(即闭包特性)。
  • 轻量化:适合短逻辑、一次性使用的场景,避免为了小逻辑单独定义一个全局 / 局部函数,减少命名污染。

为什么需要匿名函数?

避免为了简单逻辑(比如排序规则、回调逻辑)单独创建一个有名字的函数,让代码更紧凑。

方便在高阶函数(比如std::sortstd::find_if)中直接传入自定义逻辑,不用额外写函数对象或函数指针。

C++11 引入 Lambda 表达式,是 C++ 里唯一的匿名函数形式

3.2 基础语法与使用

Lambda表达式的基础语法格式如下:

cpp 复制代码
[捕获列表](参数列表) mutable -> 返回值类型 {
    // 函数体(逻辑实现)
};
部分 作用
[捕获列表] ✅ 灵魂!定义如何访问外部作用域变量(值 / 引用 /this 等)
(参数列表) 和普通函数参数一致,无参数时可省略()
mutable 可选,允许修改值捕获 的变量(默认值捕获变量是const
-> 返回值类型 可选,编译器可自动推导,复杂场景需显式指定
{函数体} 匿名函数的核心逻辑,和普通函数体完全一致

Lambda表达式语法使用层而言没有类型,所以我们一般是用auto或者模板参数定义的对象去接收。

cpp 复制代码
int main()
{
	auto add1 = [](int x, int y)->int {return x + y; };
	std::cout << add1(1, 2) << std::endl;


	// 1、捕捉为空也不能省略
	// 2、参数为空可以省略
	// 3、返回值可以省略,可以通过返回对象⾃动推导
	// 4、函数体不能省略
	auto func1 = []
	{
		std::cout << "hello bit" << std::endl;
		return 0;
	};
	func1();
	int a = 0, b = 1;
	auto swap1 = [](int& x, int& y)
	{
		int tmp = x;
		x = y;
		y = tmp;
	};
	swap1(a, b);
	std::cout << a << ":" << b << std::endl;
	return 0;
}

3.3 捕获列表

Lambda表达式中默认只能用函数体、全局变量和参数列表中的变量,如果想用外层作用域中的变量就需要进行捕获,捕获方式主要有以下几种:

捕获方式 语法 特点
值捕获 [x, y] 拷贝外部变量,Lambda 内修改不影响外部,生命周期与 Lambda 绑定
引用捕获 [&x, &y] 直接引用外部变量,修改会同步到外部,需保证变量生命周期长于 Lambda
隐式值捕获 [=] 自动拷贝函数体中用到的外部变量
隐式引用捕获 [&] 自动引用函数体中用到的外部变量
混合捕获 [=, &x] 全部值捕获,仅x用引用捕获;[&, y] 同理
捕获this [this] 类内 Lambda 访问类的成员变量 / 函数

值捕获的对象不能更改,但是引用捕获的变量可以进行修改。

cpp 复制代码
int main()
{
	// 只能⽤当前lambda局部域和捕捉的对象和全局对象
	int a = 0, b = 1, c = 2, d = 3;
	auto func1 = [a, &b]
	{
		// 值捕捉的变量不能修改,引⽤捕捉的变量可以修改
		//a++;
		b++;
		int ret = a + b;
		return ret;
	};
	std::cout << "b:" << b << std::endl << "函数调用结果:" << func1() << std::endl;
	return 0;
}

捕捉列表中混合使用隐式捕捉和显示捕捉。[=,&x]表示其他变量隐式值捕捉, x引用捕捉;[&,x,y]表示其他变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第一个元素必须是 &或=,并且&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引用捕捉。

Lambda表达式如果在函数局部域中,他可以捕捉lambda位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉lambda表达式中可以直接使用。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。

cpp 复制代码
int x = 10;
int main()
{
    // 局部的静态和全局变量不能捕捉,也不需要捕捉
    static int m = 0;
    auto func6 = []
    {
        int ret = x + m;
        return ret;
    };
    std::cout << "func6:" << func6() << std::endl;
	return 0;
}

默认情况下,lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改, mutable加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以 修改了,但是修改的只是外部变量的拷贝,形参改变不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。

cpp 复制代码
int main()
{
    int a = 0, b = 1, c = 2, d = 3;
    auto func7 = [=]()mutable
    {
        a++;
        b++;
        c++;
        d++;
        return a + b + c + d;
    };
    std::cout << func7() << std::endl;
    std::cout << a << " " << b << " " << c << " " << d << std::endl;
	return 0;
}

3.4 lambda的原理

lambda的原理和范围for很像,编译后从汇编指令层的角度看,压根就没有lambda 和范围for 这样的东西。范围for底层是迭代器,而lambda底层是仿函数对象,也就说我们写了一个lambda 以后,编译器会生成一个对应的仿函数的类。

仿函数的类名是编译按一定规则生成的,保证不同的lambda生成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是lambda类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传那些对象。

cpp 复制代码
class Rate
{
public:
    Rate(double rate)
        : _rate(rate)
    {}
    double operator()(double money, int year)
    {
        return money * _rate * year;
    }
private:
    double _rate;
};
int main()
{
    double rate = 0.49;
    // lambda
    auto r2 = [rate](double money, int year) {
        return money * rate * year;
    };
    // 函数对象
    Rate r1(rate);
    r1(10000, 2);
    r2(10000, 2);
    return 0;
}

四、包装器

C++11 包装器核心是 std::function、std::bind用于统一可调用对象类型、绑定参数、适配成员函数,解决泛型与回调中的类型不统一问题。

C++11 包装器均在 <functional> 头文件中,主要解决:

  • 不同可调用对象(函数、仿函数、lambda、成员函数)类型不统一,无法放入同一容器或作为统一回调
  • 成员函数调用需绑定对象 / 指针,接口繁琐

4.1 funciton

4.1.1 基本语法与使用

std::function 是⼀个类模板,也是一个包装器。

cpp 复制代码
template <class Ret, class... Args>
class function<Ret(Args...)>;

语法规则,需要注意的是尖括号内是函数签名(不是函数指针),要可调用对象的返回值、参数个数 / 类型匹配,就能被包装

cpp 复制代码
std::function<返回值类型(参数1类型, 参数2类型, ...)> 包装器对象;

举个例子,下面function内的函数签名就表示包装一个接收2个int、返回1个int的可调用对象

cpp 复制代码
std::function<int(int, int)> func;

std::function 的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、 lambda 、bind表达式等,存储的可调用对象被称为 std::function 的目标。若std::function 不含目标,则称它为空。调用空 std::function 的目标会抛出std::bad_function_call异常。

函数指针、仿函数、 lambda 等可调用对象的类型各不相同, std::function 的优势就是统一类型,对他们都可以进行包装,这样在很多地方就很方便声明可调用对象的类型

cpp 复制代码
#include<functional>

int f(int a, int b)
{
	return a + b;
}

struct Functor
{
public:
	int operator() (int a, int b)
	{
		return a + b;
	}
};

int main()
{
	// 包装各种可调⽤对象
	//1.包装函数指针
	std::function<int(int, int)> f1 = f;
	//2.包装仿函数
	std::function<int(int, int)> f2 = Functor();
	//3.包装Lambda
	std::function<int(int, int)> f3 = [](int a, int b) {return a + b; };

	std::cout << f1(1, 1) << std::endl;
	std::cout << f2(1, 1) << std::endl;
	std::cout << f3(1, 1) << std::endl;
	return 0;
}

除此之外,function包装器还能包装类内的成员函数(包括普通成员函数与静态成员函数),但是在语法格式上会有一点小差别。无论是包装静态成员函数还是包装普通成员函数都必须通过**&类名::** 指定类域,包装静态成员函数的时候可以省略&符号但是普通成员函数不能省略。

还有一点需要注意的就是:普通成员函数的时候参数列表会默认存在this指针,在写function包装器的函数签名时不要忘记。

cpp 复制代码
class Plus
{
public:
	Plus(int n = 10)
		:_n(n)
	{}
	static int plusi(int a, int b)
	{
		return a + b;
	}
	double plusd(double a, double b)
	{
		return (a + b) * _n;
	}
private:
	int _n;
};

int main()
{
	// 包装静态成员函数
    // 成员函数要指定类域并且前⾯加&才能获取地址

	std::function<int(int, int)> f4 = &Plus::plusi;
	std::cout << f4(1, 1) << std::endl;

	// 包装普通成员函数
	// 普通成员函数还有⼀个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以

	std::function<double(Plus*, double, double)> f5 = &Plus::plusd;
	Plus pd;
	std::cout << f5(&pd, 1.1, 1.1) << std::endl;
	return 0;
}

这里有个小技巧:普通成员函数还有⼀个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以,所以相比于先创建一个类对象然后再传递对象的指针我们也可以直接传一个匿名对象过去,但是前提是function对应的函数签名处是类名而不是类的指针。

除了上述两种传递参数的方法,传递右值引用也是可以的。

cpp 复制代码
class Plus
{
public:
	Plus(int n = 10)
		:_n(n)
	{}
	static int plusi(int a, int b)
	{
		return a + b;
	}
	double plusd(double a, double b)
	{
		return (a + b) * _n;
	}
private:
	int _n;
};

int main()
{
	Plus pd;

	std::function<double(Plus, double, double)> f6 = &Plus::plusd;
	std::cout << f6(pd, 1.1, 1.1) << std::endl;

	std::function<double(Plus&&, double, double)> f7 = &Plus::plusd;
	std::cout << f7(std::move(pd), 1.1, 1.1) << std::endl;
	std::cout << f7(Plus(), 1.1, 1.1) << std::endl;
	return 0;
}

4.2 bind

在开发中,我们经常遇到这些需求:

  1. 想固定函数的部分参数,生成一个参数更少的新函数;
  2. 想调整函数参数的传入顺序;
  3. 普通函数适配器无法调用类的非静态成员函数(因为有隐藏的 this 指针);
  4. 想把复杂函数简化后,存入容器 / 作为回调。

std::bind就是专门解决这些问题的万能适配器。

std::bind 是 C++11 提供的函数适配器(定义在 <functional> 头文件),核心作用是对可调用对象(函数、lambda、成员函数等)进行参数绑定、顺序重排、对象绑定,生成一个新的可调用对象,支持延迟调用。

它是 C++ 回调、函数封装、适配成员函数的核心工具,也是 std::function 的最佳搭档。

4.2.1 基本语法

头文件:#include <functional>

std::bind会返回一个匿名的可调用对象,可以直接调用,或用std::function包装。

cpp 复制代码
// 原型
template <class F, class... Args>
auto bind(F&& 可调用对象, Args&&... 绑定参数) -> 新可调用对象;

// 简化写法
auto 新函数 = bind(原函数, 固定值/占位符...);
cpp 复制代码
using namespace std;
int Sub(int a, int b)
{
	return (a - b) * 10;
}
int SubX(int a, int b, int c)
{
	return (a - b - c) * 10;
}

int main()
{
	//提前将参数与函数对象绑定生成一个可调用对象
	auto sub1 = bind(Sub,10, 5);
	auto sub2 = bind(SubX, 10, 5, 1);

	//直接调用可调用对象
	cout << sub1() << endl;
	cout << sub2() << endl;

	return 0;
}

除了直接给函数绑定所有参数后续延迟调用外bind还支持占位符,bind的占位符是实现动态传参、参数重排的核心,专门用来标记调用新函数时才传入的动态参数,是 bind 最关键的组成部分。

简而言之占位符可以在bind的时候预留动态参数的位置,这几个参数不会被提前绑定而是再后续调用的时候由用户自己动态传入。下面我们来详细了解一下:

占位符全称std::placeholders::_1std::placeholders::_2std::placeholders::_3 ...

其中placeholders表示在std下的一个命名空间,在使用的时候几乎不会直接写占位符的全称而是提前展开其所属的命名空间方便使用。

cpp 复制代码
#include <functional>
// 简化占位符写法(必加)
using namespace std::placeholders; 

_1 / _2 / _3 的数字 = 新函数调用时的参数位置

  • _1 → 新函数的第 1 个参数
  • _2 → 新函数的第 2 个参数
  • _3 → 新函数的第 3 个参数

我们来看下面的代码:

cpp 复制代码
using namespace std;
using namespace std::placeholders;
int Sub(int a, int b)
{
	return (a - b) * 10;
}

int main()
{
	auto sub1 = bind(Sub,_1, _2);
	auto sub2 = bind(Sub,_2, _1);

	cout << sub1(10,5) << endl;
	cout << sub2(10,5) << endl;

	return 0;
}

这里我们首先利用bind绑定了Sub函数但是其参数我们先利用占位符占位留给后续用户调用时动态传入,但是我们在这里发现了一个问题就是看似我们传入的参数都是一致的为什么结果不一样呢?其实在传参的时候发生了以下的过程:

_1,_2表示的是新函数(也就是bind完生成的可调用对象)的第一个与第二个参数,但是在向上给Sub传递的时候是按照顺序传参的。

有了占位符,我们使用bind对参数进行控制就非常方便了,可以直接给函数绑定固定需要的参数对于一些需要后续动态传递的参数我们则可以利用占位符暂时占位。当然我们也可以利用上面占位符的特性对参数进行重排:

cpp 复制代码
using namespace std;
using namespace std::placeholders;
int Sub(int a, int b)
{
	return (a - b) * 10;
}
int SubX(int a, int b, int c)
{
	return (a - b - c) * 10;
}

int main()
{
    // 调整参数个数(常⽤)
    auto sub3 = bind(Sub, 100, _1);             //Sub(100,5)
    cout << sub3(5) << endl;

    auto sub4 = bind(Sub, _1, 100);             //Sub(5,100)
    cout << sub4(5) << endl;

    // 分别绑死第123个参数
    auto sub5 = bind(SubX, 100, _1, _2);        //SubX(100,5,1)
    cout << sub5(5, 1) << endl;

    auto sub6 = bind(SubX, _1, 100, _2);        //SubX(5,100,1)
    cout << sub6(5, 1) << endl;

    auto sub7 = bind(SubX, _1, _2, 100);        //SubX(5,1,100)
    cout << sub7(5, 1) << endl;

	return 0;
}

4.2.2 绑定成员函数

类的成员函数分为两种静态成员函数与非静态成员函数,非静态成员函数自带隐藏的 this 指针,无法直接调用,bind 可以绑定对象 / 指针,完美适配成员函数。

与包装器类似,bind绑定类的成员函数时也需要指定类域。绑定非静态成员函数的格式如下:

cpp 复制代码
bind(&类名::成员函数, 对象指针/对象/引用, 占位符...);
  • 第一个参数:成员函数地址(必须加 &);
  • 第二个参数:类的实例对象 / 指针 / 智能指针(提供 this);
  • 后续参数:成员函数的参数 / 占位符。

绑定静态成员函数的时候指定类域时&可加可不加,而且静态成员函数没有this指针所以也就不需要传递类的实例对象 / 指针 / 智能指针等,所以这时使用方法如下:

cpp 复制代码
bind(&类名::成员函数,占位符...);
cpp 复制代码
#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;

class Calc {
public:
    int mul(int a, int b) {
        return a * b;
    }
    static int sub(int c, int d)
    {
        return c - d;
    }
};

int main() {

    // 绑定成员函数:&Calc::mul + 对象指针(匿名对象提供) + 占位符
    auto calc_mul = bind(&Calc::mul, Calc(), _1, _2);

    //bind静态成员函数指定类域时&可加可不加
    auto calc_sub1 = bind(&Calc::sub,  _1, _2);
    auto calc_sub2 = bind(Calc::sub,  _1, _2);

    // 调用新函数,传入两个参数
    cout << calc_mul(4, 5) << endl;  // 20
    cout << calc_sub1(4, 5) << endl; // -1
    cout << calc_sub2(4, 5) << endl; // -1
    return 0;
}

bind绑定成员方法后通常搭配function包装器来使用,bind 生成的匿名对象,用 std::function 包装后,可以统一类型、存入容器、作为回调,这是工业级开发的标准用法。

cpp 复制代码
int main() {

    // 绑定成员函数:&Calc::mul + 对象指针(匿名对象提供) + 占位符
    std::function<int(int,int)> calc_mul = bind(&Calc::mul, Calc(), _1, _2);

    //bind静态成员函数指定类域时&可加可不加
    std::function<int(int, int)> calc_sub1 = bind(&Calc::sub,  _1, _2);
    std::function<int(int, int)> calc_sub2 = bind(Calc::sub,  _1, _2);

    // 调用新函数,传入两个参数
    cout << calc_mul(4, 5) << endl;  // 20
    cout << calc_sub1(4, 5) << endl; // -1
    cout << calc_sub2(4, 5) << endl; // -1
    return 0;
}

不同于直接使用function包装器包装类的成员函数,使用bind提前声明类域并提供this指针后function的函数签名中就不需要再指定和后续提供了。

cpp 复制代码
class Calc {
public:
    int mul(int a, int b) {
        return a * b;
    }
};

int main() {

    // 绑定成员函数后再包装不需要再指定类域并提供this指针了
    std::function<int(int,int)> calc_mul1 = bind(&Calc::mul, Calc(), _1, _2);

    //直接包装需要指定类域并提供this指针
    std::function<int(Calc&&,int, int)> calc_mul2 = &Calc::mul;

    // 调用新函数,传入两个参数
    cout << calc_mul1(4, 5) << endl;  // 20
    //this指针由匿名对象提供
    cout << calc_mul2(Calc(),4, 5) << endl;  // 20
    return 0;
}

五、empalce系列接口

5.1 与传统接口不同点

C++11 为所有标准容器引入了 emplace 系列接口,是现代 C++ 开发中性能优化的核心工具。核心定位:就地构造元素,彻底避免临时对象的拷贝 / 移动开销,完美替代传统的 push_back/insert/push_front

先看一下传统的插入接口有什么不足,以pusn_back为例:

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

struct Person {
    int id;
    string name;
    // 构造函数
    Person(int i, string n) : id(i), name(n) {
        cout << "Person(int i, string n) 构造" << endl;
    }
    // 拷贝构造
    Person(const Person& other) : id(other.id), name(other.name) {
        cout << "Person(const Person& other) 拷贝构造" << endl;
    }
	
};

int main() {
    vector<Person> vec;
    // 传统插入:创建临时对象 → 拷贝到容器 → 销毁临时对象
    vec.push_back(Person(1, "张三")); 
    return 0;
}

这段代码首先调用构造函数构造临时对象,然后将临时对象拷贝进容器的内存空间中。对于内置类型编译器会一字节一字节地拷贝,对于自定义类型会调用它的拷贝构造。如果该自定义类型明确地开辟了资源就需要完成深拷贝性能损耗很大。

除了额外调用拷贝构造外,当我们显式地定义移动构造地时后对于一些需要深拷贝的类型编译器会走移动构造:

cpp 复制代码
struct Person {
    int id;
    string name;
    // 构造函数
    Person(int i, string n) : id(i), name(n) {
        cout << "Person(int i, string n) 构造" << endl;
    }
    // 拷贝构造
    Person(const Person& other) : id(other.id), name(other.name) {
        cout << "Person(const Person& other) 拷贝构造" << endl;
    }
	//移动构造
    //这里将other.name转化为右值触发std::string的移动构造
	Person(const Person&& other) : id(other.id), name(std::move(other.name)) {
        cout << "Person(const Person& other) 移动构造" << endl;
    }
};

int main() {
    vector<Person> vec;
    // 传统插入:创建临时对象 → 拷贝到容器 → 销毁临时对象
    vec.push_back(Person(1, "张三")); 
    return 0;
}

上面两种情况的共同点就是首先构造了一个临时对象,然后临时对象再走移动构造或者拷贝构造(移动构造没有的话)。那么为什么会这样呢?其中本质的原因在于push_back的参数类型已经随着容器的模板参数的确定已经确定好了,这就说明push_back它只支持插入T(容器模板参数)类型的对象,如果没有则会先构造出一个临时对象然后再插入。

但是empalce系列的接口则不一样,以emplace_back来讲它是一个可变参数模板:

cpp 复制代码
template <class... Args>
void emplace_back(Args&&... args)
{
    // 内部调用:
    ::new((void*)ptr) T(std::forward<Args>(args)...);
}

它除了可以接收对象,它也可以接收构造函数需要的参数然后将参数完美转发给类的构造函数,此时构造函数会利用传过来的参数直接在容器对应的内存构造出对象。

所以上述的代码利用emplace_back接口应该这样使用:

cpp 复制代码
int main() {
    vector<Person> vec;
    // 传统插入:创建临时对象 → 拷贝到容器 → 销毁临时对象
    vec.emplace_back(1, "张三"); 
    return 0;
}

5.2 与传统接口相同点

emplace_back除了可以直接接受该容器构造函数的参数外,它还支持接受已经构造好的类类型对象进行插入,只不过这时它与传统接口(insert、push_back、push_front)没有区别:

cpp 复制代码
int main() {
    vector<Person> vec;

    vec.emplace_back(Person(2, "赵六"));

    return 0;
}
相关推荐
cch89182 小时前
PHP vs Java:主流编程语言深度对比
java·开发语言·php
少司府2 小时前
C++基础入门:类和对象(上)
c语言·开发语言·c++·类和对象·访问限定符
deep_drink2 小时前
1.1、Python 与编程基础:开发环境、基础工具与第一个 Python 项目
开发语言·人工智能·python·llm
REDcker2 小时前
C++ new、堆分配与 brk / mmap
linux·c++·操作系统·c·内存
丸辣,我代码炸了2 小时前
如何手搓序列化器(以java为例)
java·开发语言·kafka
阿阿阿阿里郎2 小时前
C++面向对象--类、模板
c++
William_wL_2 小时前
【C++】list的使用
c++
快乐柠檬不快乐2 小时前
基于Java+SpringBoot+SSM攻防靶场实验室平台
java·开发语言·spring boot
lly2024062 小时前
SQL AND & OR 操作符详解
开发语言
伐尘2 小时前
【图形学】CS:GO 的 “Uber 着色器” 是啥?
开发语言·golang·着色器