
文章目录
前言
上一篇博客重点讲解了可变参数模板,主要在于可变 二字。以往我们写函数一般都会把参数写死(也就是说传过去的参数的个数被固定了),这样反倒不灵活。有了可变参数模板,这就使得我们可以传任意个参数过去,增加了我们代码的灵活性。接下来我们看看可变参数模板是怎么在我们STL接口中运用起来的。
提示:以下是本篇文章正文内容,下面案例可供参考
1️⃣一、emplace接口(重点)
可以这么说,可变参数是emplace的语法基石 ,而emplace就是可变参数模板的经典落地实战
1.1push/insert为何低效?
-
在emplace接口诞生之前,我们向容器插入数据通常都是调用push_back/push_front等接口进行操作,这样的操作固然没有问题,但却无形之中增加了不必要的临时对象的创建和销毁
-
即使push/insert重载了右值引用的版本
push_back(T&& x)(右值引用可以抢夺资源,调用移动构造,减少拷贝),看似解决了性能问题,但也逃不了内存的开销
我们先分C++11前与后关于插入接口的效率对比
①C++11前,只有左值引用的版本push(T& x)
cpp
std::vector<std::string> vec;
// 传入字符串字面量,编译器先构造临时std::string对象
vec.push_back("hello emplace");
- 先构造一个临时的string对象"hello emplace",第一步开销
- 再调用拷贝构造,分配内存,把这个临时对象拷贝进容器中,第二步开销
- 再销毁原先创建的临时对象
这个过程是显而易见的构造-拷贝-销毁的过程,几乎是冗余操作,尤其是到了自定义类型的对象,如
string等,开销更大
②C++11后,重载右值版本进行移动构造push(T&& x)
cpp
std::vector<std::string> vec;
// C++11后,匹配push_back(std::string&&),调用移动构造
vec.push_back("hello emplace");
- 以"hello emplace"为参数,构造一个临时对象(分配内存,拷贝内容)
- 调用std::string的移动构造函数,将临时对象的资源所有权转移到容器的内存空间中(仅修改指针指向,无堆内存分配、无内容拷贝,开销极小,但并非为 0)
- 函数调用结束后,销毁临时的 std::string 对象(此时临时对象已无资源,销毁仅做简单清理 ------ 开销极小,但并非为 0)
核心痛点:传统接口的设计缺陷 ------ 只能接收 "已构造的对象"
- 无论是 C++11 前的拷贝构造,还是 C++11 后的移动构造,传统 push/insert 接口的底层设计缺陷从未改变 :它们的参数是已构造完成的 T 类型对象(左值或右值),而非构造 T 对象所需的参数
- 这就决定了:只要通过传统接口插入 "未提前构造的对象",就必须先在外部构造一个临时对象(左值 / 右值),再将这个对象传递给接口------ 临时对象的创建是无法通过移动语义规避的,这是传统接口的先天限制。
1.2emplace的核心原理
基于上面的分析,那有没有能够跳出临时对象的构建与销毁,直接在目标容器里构造对象的方法呢?答案就是emplace接口。
emplace 系列接口的设计思路很简单:不接受已构造的对象,而是接受构造对象所需的所有参数,然后在容器的目标内存位置,直接调用对象的构造函数完成构造。
而实现这一个思路,必须得解决两个问题
- ①我们传的参数要对应匹配该容器的构造函数,所以就注定了我们传的参数个数、类型是随机的(因为要方便我们调用合适的构造函数)------可变参数模板 就解决了这个问题,通过
Args&&... agrs参数包,可以让emplace接受任何类型、个数的参数 - ②要保证传递给构造函数的参数类型不被破坏(左值还是左值,右值还是右值),避免不必要的拷贝 ------完美转发
(std::forward<Args>(args)...)解决了这个问题,实现参数的无损传递。(上文提过,右值引用的变量具有左值属性)
cpp
// 1. 极简待构造的类
class Product {
public:
string name;
int price;
// 带参构造(仅保留核心)
Product(string n,int p = 1)
:name(n)
,price(p)
{ }
};
// 2. 极致简洁的容器类(仅含emplace核心)
class SimpleContainer {
public:
// 核心emplace接口:可变参数模板接收构造参数
template <typename... Args>
void emplace(Args&&... args) {
// 直接用参数构造对象(emplace本质:传构造参数,而非现成对象)
obj = Product(forward<Args>(args)...);
}
// 方便验证结果(非核心,仅辅助)
Product get_obj() { return obj; }
private:
Product obj; // 存储的对象(简化为直接成员,无内存分配)
};
// 测试:仅调用emplace
int main() {
SimpleContainer container;
// emplace直接传构造参数(不用先造Product对象)
container.emplace("耳机", 199);
// 验证
Product p = container.get_obj();
cout << "emplace构造的对象:" << p.name << " | 价格:" << p.price << endl;
return 0;
}
- emplace 函数体里的 "传值" 不是 "额外开销",而是构造容器内对象的「必要步骤」;
- push_back 的核心问题是 "多一次外部临时对象的构造 / 销毁",这是「冗余开销」;
- 对普通开发者来说,不用纠结 "单次传值的微小开销",只要记住:传构造参数用 emplace,传现成对象用 push_back,这就够了。
2️⃣二、lambda
2.1lambda表达式语法
lambda表达式本质是一个匿名函数对象,跟普通函数不同得是他可以定义在函数内部。lambda表达式语法使用层而言没有类型,所以我们一般是用auto去接受lambda对象lambda表达式的格式:[capture-list](parameters)->return type{function body}[capture-list]:捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的带啊吗是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用,捕捉列表可以传值和传引用捕捉。捕捉列表的[]不能省略(parameters):参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以联通()一起省略->return type:返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可以省略。一般情况下返回值类型明确情况下,也可省略,由编译器对返回类型进行推到{function body}:函数体,不可省略
cpp
int main()
{
//lambda表达式
auto add = [](int x, int y)->int {return x + y; };
std::cout << add(1, 2) << std::endl;
auto func = [] {std::cout << "hello czh" << std::endl; };
func();
int a = 0, b = 1;
auto swap = [](int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
};
swap(a, b);
std::cout << a << ":" << b << std::endl;
return 0;
}
2.2捕捉列表
lambda表达式中默认只能用lambda函数体和参数中的变量,如果想用外层作用域的变量就要进行捕捉- 第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分隔。
[x,y,&z]表示x和y通过值捕捉,z通过传引用捕捉 - 第二种就是隐式捕捉,
[=],[&],这样我们在函数体里面使用什么变量,就会捕捉什么变量,所有的变量都是对应的值(=)捕捉和引用(&)捕捉 - 第三种捕捉方式就是混合捕捉。
[=,&x,&y]就代表我使用的变量除了x和y是传引用捕捉之外,其余都是值捕捉。当使用混合捕捉时,第一个元素必须是=或& lambda的值捕捉的元素默认是被const修饰的
cpp
int x = 0;
auto func1 = []()
{
x++;
};
int main()
{
//lambda表达式
auto add = [](int x, int y)->int {return x + y; };
std::cout << add(1, 2) << std::endl;
auto func = [] {std::cout << "hello czh" << std::endl; };
func();
int a = 0, b = 1;
auto swap = [](int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
};
swap(a, b);
std::cout << a << ":" << b << std::endl;
int a = 0, b = 1, c = 2, d = 3;
auto func1 = [a, &b]//a值捕捉,b引用捕捉
{
b++;
int ret = a + b;
return ret;
};
std::cout << func1() << std::endl;
auto func2 = [=]//使用到的变量均是值捕捉
{
int ret = a + b + c;
return ret;
};
std::cout << func2() << std::endl;
auto func3 = [&]//使用到的变量均是引用捕捉
{
a++;
b++;
};
func3();
cout << a << b;
return 0;
}
3️⃣三、包装器
3.1function
cpp
template<class T>
class function;
template<class Ref,class... Args>
class function<(Ref(Args...))>;
std::function是一个类模板,也是一个包装器。std::function的实力对象可以包装存储其他的可以调用对象,包括函数指针,仿函数,lambda,bind表达式等,存储的可调用对象被称为std::function的目标。若std::function不含目标,则称它为空。调用空会抛出异常- 函数指针、仿函数、lambda等可调用对象的类型各不相同,
std::function的优势就是统一类型,对他们都可以进行包装,这样在很多地方就方便声明可调用对象的类型
cpp
int f(int a, int b)
{
return a + b;
}
struct func
{
public:
int operator()(int a, int b)
{
return a + b;
}
};
class Plus
{
public:
Plus(int n = 10)
:_n(n)
{ }
static int plusi(int a, int b)//static修饰成员变量属于类本身,只能访问静态成员
{
return a + b;
}
double plusd(double a, double b)
{
return (a + b) * _n;
}
private:
int _n;
};
int main()
{
function<int(int, int)> f1 = f;
func fc;
function<int(int, int)> f2 = fc;
function<int(int, int)> f3 = [](int a, int b) {return a + b; };
cout << f1(1, 2) << endl;
cout << f2(3, 4) << endl;
cout << f3(5, 6) << endl;
function<int(int, int)> f4 = &Plus::plusi;
cout << f4(1, 1) << endl;
function<double(Plus*, double, double)> f5 = &Plus::plusd;//要通过&取到函数的指针
function<double(Plus, double, double)> f6 = &Plus::plusd;//可以传对象,也可以传对象的地址,也可以传右值
function<double(Plus&&, double, double)> f7 = &Plus::plusd;
Plus pd;
cout << f5(&pd, 1.1, 1.1) << endl;
cout << f6(pd, 1.1, 1.1) << endl;
cout << f7(move(pd), 1.1, 1.1) << endl;
cout<<f7(Plus(), 1.1, 1.1) << endl;
return 0;
}
3.2bind
bind是一个函数模板,它也是一个可调用对象的包装器,可以把他看作一个函数适配器,对接受的fn可调用对象进行处理后返回一个可调用对象。bind可以用来调整参数个数和参数顺序。bind也在functional这个头文件- 调用bind的一般形式:
auto newcallable = bind(callbable,arg_list);其中newcallable本身也是一个可调用对象,arg_list是一个逗号分割的参数列表,对应给定的callable的参数。当我们调用newcallable,newcallable会调用callable,并传给它arg_list中的参数 - arg_list中的参数可能包含形如_n的名字,其中n是⼀个整数,这些参数是占位符,表⽰
newCallable的参数,它们占据了传递给newCallable的参数的位置。数值n表⽰⽣成的可调⽤对象中参数的位置:_1为newCallable的第⼀个参数,_2为第⼆个参数,以此类推。_1/_2/_3...这些占位符放到placeholders的⼀个命名空间中
cpp
#include <iostream>
#include <functional>
using namespace std;
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int Sub(int a, int b)
{
return (a - b) * 10;
}
int SubX(int a, int b, int c)
{
return (a - b - c) * 10;
}
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
auto sub1 = bind(Sub, _1, _2);
cout << sub1(10, 5) << endl;
// bind 本质返回的一个仿函数对象
// 调整参数顺序(不常用)
// _1 代表第一个实参
// _2 代表第二个实参
// ...
auto sub2 = bind(Sub, _2, _1);
cout << sub2(10, 5) << endl;
// 调整参数个数(常用)
auto sub3 = bind(Sub, 100, _1);
cout << sub3(5) << endl;
auto sub4 = bind(Sub, _1, 100);
cout << sub4(5) << endl;
// 分别绑死第1、2、3个参数
auto sub5 = bind(SubX, 100, _1, _2);
cout << sub5(5, 1) << endl;
auto sub6 = bind(SubX, _1, 100, _2);
cout << sub6(5, 1) << endl;
auto sub7 = bind(SubX, _1, _2, 100);
cout << sub7(5, 1) << endl;
// 成员函数对象进行绑死,就不需要每次都传递了
function<double(Plus&&, double, double)> f6 = &Plus::plusd;
Plus pd;
cout << f6(move(pd), 1.1, 1.1) << endl;
cout << f6(Plus(), 1.1, 1.1) << endl;
// bind 一般用于,绑死一些固定参数
function<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2);
cout << f7(1.1, 1.1) << endl;
// 计算复利的 lambda
auto func1 = [](double rate, double money, int year)->double {
double ret = money;
for (int i = 0; i < year; i++)
{
ret += ret * rate;
}
return ret - money;
};
// 绑死一些参数,实现出支持不同年华利率,不同金额和不同年份计算出复利的结算利息
function<double(double)> func3_1_5 = bind(func1, 0.015, _1, 3);
function<double(double)> func5_1_5 = bind(func1, 0.015, _1, 5);
function<double(double)> func10_2_5 = bind(func1, 0.025, _1, 10);
function<double(double)> func20_3_5 = bind(func1, 0.035, _1, 30);
cout << func3_1_5(1000000) << endl;
cout << func5_1_5(1000000) << endl;
cout << func10_2_5(1000000) << endl;
cout << func20_3_5(1000000) << endl;
return 0;
}
总结
C++11到这里就先告一段落了,希望大家能够从这篇文章真真实实的学到东西,不懂可以私信博主交流噢