文章目录
📖 前言
上一篇我们已经将C++11开了个头,详细讲述了C++11中的 { } 列表初始化和右值引用,本章将继续讲解C++11中的一些新的且比较实用的功能。
列表初始化 + 右值引用复习:👉 传送门
1. 可变参数模板
C++11的新特性可变参数模板:
- 能够让创建可以接受可变参数的函数模板和类模板,相比C++98 / C++03
- 类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进
- 然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的
1.1 万能模板:
万能模板的有样式:
- 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值
- 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力
- 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值
很显然这并不是我们想要的,因为无论传的是左值还是右值,都将退化成左值:
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
//std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
//完美转发,按照原封不动的方式进行转发
//Fun(t);
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10); //右值
int a;
PerfectForward(a); //左值
PerfectForward(std::move(a)); //右值
const int b = 8;
PerfectForward(b); //const 左值
PerfectForward(std::move(b)); //const 右值
return 0;
}
如图所示全部都是调用左值引用了,这可能是编译器底层实现的原因,但是这显然不是一个好的现象。
1.2 完美转发:
我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的
完美转发:
C++11新提供的forward函数模板,可以解决上述问题,使得传右值不会退化成左值,。
1.3 可变参数模板的使用:
首先在学C语言的时候,我们用的printf
函数是一个可变参数函数,printf
从语法上说是可以写多个参数,然后该函数自己识别。
可变参数模板:
解释:
- 可变模板参数,源自于printf(可变参数函数),…代表可变参数
- Args是一个模板参数包,args是一个函数形参参数包
- 声明一个参数包Args…args,这个参数包中可以包含0到任意个模板参数
模板的可变类型参数,不仅不知道有多少个,还不知道类型,要推演出来。
template <class ...Args>
void ShowList1(Args... args)
{
//参数个数
cout << sizeof...(args) << endl;
}
//不一定非要用Args也可以取别的名字
template <class ...X>
void ShowList2(X... y)
{
cout << sizeof...(y) << endl;
}
int main()
{
ShowList1(1, 'x', 1.1);
ShowList2(1, 2, 3, 4, 5);
return 0;
}
运行结果是:3 5
参数包展开:
//方法一:
//递归到最后一个,就找最匹配的那一个
template <class T>
void ShowList(const T& val)
{
cout << val << "->" << typeid(val).name() << "end" << endl;
}
//编译时递归去推
//整个推导的过程是在编译的时候进行的(这是个编译的过程 -- 编译时决议)
template <class T, class ...Args>
void ShowList(const T& val, Args... args)
{
cout << sizeof...(args) << endl;
cout << val << "->" << typeid(val).name() << endl;
//去递归解析
ShowList(args...);
}
//方法二:
//0个模板参数走这个 -- 作为递归的终结
void ShowList()
{}
int main()
{
ShowList(1, 'x', 1.1);
cout << endl;
//有几个参数都能推
ShowList(1, 2, 3, 4, 5);
return 0;
}
- 类似于递归的一个过程,但是不是递归,最后都有一个结束条件,参数包中的模板参数是一个一个减少的。
- 整个推导的过程是在编译的时候进行的(这是个编译的过程 – 编译时决议)
用参数包定义数组:
template <class ...Args>
void ShowList(Args... args)
{
//*列表初始化 -- 这种方法不通用
//C++只允许数组里面是同一个类型的
int arr[] = { args... };
cout << endl;
}
int main()
{
//ShowList(1, 'x', 1.1, vector<int>{2, 2});这种就不适用 -》 int arr[] = { args... };
//cout << endl;
ShowList(1, 2, 3, 4, 5);
ShowList(10, 20, 30);
return 0;
}
template <class T>
int PrintArg(const T& t)
{
cout << t << " ";
return 0;
}
template <class ...Args>
void ShowList(Args... args)
{
//列表初始化
//int arr[] = { (PrintArg(args), 0)...};
int arr[] = { PrintArg(args)... };
cout << endl;
}
int main()
{
ShowList(1, 'x', 1.1, string("hello world"));
cout << endl;
ShowList(1, 2, 3, 4, 5);
//ShowList(10, 20, 30);
return 0;
1.4 emplace_back:
C++11STL中新增了一个尾插的方法:
- emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
- 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
- 可以不传pair,可以直接传参数
- 拿到参数自己构建对象(保证了底层一定是右值 —— 直接去构造的)
push_back():
- 参数如果给的是左值是:构造 + 拷贝构造
- 参数如果给的是右值是:构造 + 移动构造
- 函数传值返回也是个右值
emplace_back():
- 直接构造
这两者其实差距不大
2. lambda表达式
在仿函数那一节我们知道,如果我们用每次为了实现一个sort算法,因为每次比较的数据类型不同,都要重新去写一个类(仿函数),如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便,因此,在C++11语法中出现了Lambda表达式。
2.1 lambda表达式的定义:
定义了一个可以调用的对象 / 匿名函数,一般定义在局部,特点是可以深度绑定了局部的数据。
lambda表达式书写格式:
- 普通函数有函数名,lambda函数没有函数名,他是一个整体
- lambda表达式,实际上是一个匿名函数,实际上是在定义一个函数(局部的函数)
- lambda表达式通常是用来定义小函数
- lambda表达式在局部是很好用的,最特别的就是捕捉列表
auto Add1 = [](int x, int y)->int{return x + y; }; Add叫lambda表达式的对象 – lambda定义的是一个对象。
lambda表达式各部分说明:
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- (parameters): 参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。
- mutable: 默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->returntype: 返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {statement}: 函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的量。
注意:
- 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。
- 因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
2.2 lambda表达式的用法:
int main()
{
int a = 0, b = 200;
//一般是局部匿名函数 也可以写到全局(一般都不写返回值)
//参数列表(无参的时候)和返回值(表达式自动推导)也可以省略掉
auto Add1 = [](int x, int y)->double {return (x + y) / 3.0; };
auto Add2 = [](int x, int y)->int {return (x + y) / 3.0; };
//参数列表可以省略了
auto Add3 = [a, b] {return (a + b) / 3.0; };
//调用是和普通的函数一样的
cout << Add1(a, b) << endl;
cout << Add2(a, b) << endl;
//调用没有区别,但是没有实参,因为捕捉了
cout << Add3() << endl;
}
- 一般是局部匿名函数 也可以写到全局(一般都不写返回值)
- 参数列表(无参的时候)和返回值类型(表达式自动推导)也可以省略掉
- 和普通函数调用没区别,但是没有实参,因为捕捉了
2.2 - 1 捕捉列表的用法
用lambda表达式来实现交换两个数:
int main()
{
int a = 0, b = 200;
//方法一:
auto Swap1 = [](int& x, int& y)->void {
int tmp = x;
x = y;
y = tmp;
};
Swap1(a, b);
cout << a << " " << b << endl;
//方法二:
//mutable 只是让传值捕捉变量const属性去掉了
//mutable要和()参数列表配在一起
//可以认为里面的a,b还是外面a,b的拷贝 -- 里面改变外面还是不变
//所以mutable实际没什么价值
//这样写还是没有交换 -- 只是编译通过了但是达不到我们想要的效果
//auto Swap2 = [a, b]()mutable->void {
// int tmp = a;
// a = b;
// b = tmp;
//};
//用引用的方式捕捉(按值捕捉不能改变)
auto Swap2 = [&a, &b]()->void {
int tmp = a;
a = b;
b = tmp;
};
Swap2();
cout << a << " " << b << endl;
}
方法一:
- 就像普通函数那样传引用传参
方法二:
- 不能通过传值传参,首先捕捉列表捕捉的数据具有const属性,不能修改
- 其次mutable可以让传值捕捉变量const属性去掉了,即使去掉了
-
- mutable要和()参数列表配在一起
-
- 所以mutable实际没什么价值
- 可以认为里面的a,b还是外面a,b的拷贝 ,里面改变外面还是不变
所以只能按照引用的方式捕捉
各种混合捕捉:
int main()
{
int c = 2, d = 3, e = 4, f = 5, g = 6, ret;
//传值的方式捕捉全部对象
auto Func1 = [=] {
return c + d * e / f + g;
};
cout << Func1() << endl;
//传引用捕捉全部对象
auto Func2 = [&] {
ret = c + d * e / f + g;
};
Func2();
cout << ret << endl;
//混着捕捉
auto Func3 = [c, d, &ret] {
ret = c + d;
};
Func3();
cout << ret << endl;
//ret传引用捕捉 其他全部传值捕捉
auto Func4 = [=, &ret] {
ret = c + d * e / f + g;
//传值捕捉默认是加了const的
//c = 1;
};
Func4();
cout << ret << endl;
return 0;
}
2.3 lambda表达式的类型名称:
每个lambda表达式类型的名字都是不一样的,先来看一段程序:
int main()
{
auto Add = [](int x, int y)->int { return x + y; };
cout << typeid(Add).name() << endl;
return 0;
}
仿函数的名称后面叫uuid,一组随机字符串,通过某个算法得到的,使得每个lambda表达式的名字都不同。
lambda表达式的底层和仿函数的底层其实是一样的。
可以将lambda表达式赋值给相同类型的函数指针
不建议这样写。
3. 包装器
function包装器:
function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。
模板参数说明:
- Ret: 被调用函数的返回类型
- Args…: 被调用函数的形参
看下面一段程序:
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
//函数名
cout << useF(f, 11.11) << endl;
//函数对象
cout << useF(Functor(), 11.11) << endl;
//lamber表达式
cout << useF([](double d)->double{ return d / 4; }, 11.11) << endl;
return 0;
}
- func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能是lamber表达式对象?
- 所以这些都是可调用的类型!
如此丰富的类型,可能会导致模板的效率低下!
包装器可以很好的解决上面的问题:
- function可以认为是一个类模板,包装可调用对象
- 经过包装器包装之后得到的都是一个统一的类型function
- 最后count的地址相同,并且count到3了,说明包装器是统一了类型
注意,特殊的包装:
3.1 bind绑定:
简介:
std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。 一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。
可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
bind是个函数模板,调整了调用对象参数,用来调整个数和顺序,生成一个新的可调用对象:
包装器的特点就是统一了类型,但是值得注意的是包装时一定要注意匹配,如下:
这就在包装时不匹配,所在包装时一定要匹配。
下面的场景我们存一个map,这时就要求function包装器要统一了:
不过不免会有一些特殊情况:
func1和func2是可以的,但是func3就难受了呀,不匹配啊…
此时我们就可以通过bind函数来调整一下参数个数:
通过bind函数来调整顺序:
调整顺序,用处不大。
3.2 bind返回值类型:
std::bind
函数的返回类型是一个未指定的对象类型,它是一个函数对象,但具体的类型是实现相关的,并且非常复杂。
我们要根据要绑定的函数的参数和返回值来确定bind函数的返回值:
类内函数的形参有个隐藏的this指针,bind返回值的类型要根据所要绑定的函数的参数来确定。
- 不过这里也可以写成
function<void(Myclass, int, int)> / function<void(Myclass*, int, int)>
,后面的对应bind
参数给s1,和&s1都行。记忆吧,我也不知道为什么。 - 但是f在调用的时候一定要是和function尖括号中的Myclass / Myclass*匹配的。
- 例如,前面是Myclass * 的话,f 中一定传&s1 。前面是Myclassf的话,f 中一定传s1。
在C++中,成员函数的名称本身并不是一个指向函数地址的指针:
如果要将一个成员函数作为参数传递,或者在类外部使用成员函数指针,需要使用函数名取地址的方式来获取成员函数的指针。
这是因为成员函数需要通过对象来调用,所以它的实际函数签名(包括返回类型和参数列表)与普通函数不同。只有在使用成员函数指针时,才会将该函数与特定对象绑定起来,从而产生可被调用的函数指针。
类内函数指针:
3.3 bind参数匹配等问题:
std::bind
函数中的std::placeholders
的个数应该和std::function
尖活号中函数的形参个数一样。
- 这是为了确保在调用绑定的函数对象时传入的参数数量和类型与被绑定函数的形参列表匹配。
- 如果在
std::bind
中使用的占位符的数量少于函数形参的数量,那么绑定的函数对象将缺少对应的参数,导致在调用时无法提供足够的参数。 - 如果在
std::bind
中使用的占位符的数量多于函数形参的数量,那么多余的占位符不会被使用,也不会引发错误。但是,在调用绑定的函数对象时,仍需传入正确数量和类型的参数。
因此,在使用std::bind
进行函数绑定时,确保使用的占位符数量与函数形参的数量相匹配是非常重要的。
举个栗子:
运行结果:
- 在这个示例中,使用了两个占位符
std::placeholders::_1
和std::placeholders:: _2
,并且第二个参数是一个常量值 4。 - 这意味着在调用绑定的函数对象时,第一个参数会被替换为实际提供的第一个参数, 而第二个参数将始终为 4,第三个参数会被替换为实际提供的第二个参数。
- 因此,当你调用绑定的函数对象
f(1, 2, 3)
时,实际上会调用func(1, 4, 2)
。
请注意,对于占位符和常量值的组合绑定可以灵活地应用于不同的场景,根据具体需求来选择使用哪种方式进行绑定。
再来个栗子:
- 在这段代码中,在使用
std::bind
绑定成员函数func时,使用了占位符std::placeholders::_1
、std::placeholders::_3
。 - 其中,
std::placeholders::_1
表示绑定的函数参数中的第一个参数,std::placeholders::_3
表示绑定的函数参数中的第三个参数。 - 在调用绑定的函数对象f时,将&s1作为第一个参数传递给
std::placeholders::_1
,将1作为第二个参数传递给 100,将2作为第三个参数传递给std::placeholders::_3
。
因此,调用f(&s1, 1, 2)
实际上会调用Myclass::func(&s1, 100, 2)
。在函数func内部的输出语句cout << a << endl;
和cout << b << endl;
会分别输出100和2。
3.4 奇怪的用法:
- 在使用
std::bind
函数进行绑定时,第二个参数是传入要绑定成员函数的对象,也称为对象的指针或引用。
这是因为非静态成员函数需要通过对象来访问类的成员变量和其他成员函数。 -
这可能就是上述bind函数第二个传指针和引用都行的原因。(猜的)
- 当我们调用一个非静态成员函数时,需要提供该函数所属的对象,以便在函数内部可以正确地访问成员变量。
- 通过将对象作为`std::bind的第二个参数传入,实际上是将对象绑定到了正在绑定的成员函数上,使得在调用绑定后的函数对象时,它能够正确地操作对应的对象。
- 在这个例子中,MyPair 结构体中的成员函数 multiply() 不是静态成员函数,因此它需要通过一个对象来调用。
- 通过将 ten_two 对象作为
std::bind
的第二个参数传入,在绑定完之后,f() 调用时就会自动使用绑定的对象 ten_two 来调用成员函数 multiply()。 - 实际上,这里通过绑定了成员变量 a,相当于创建了一个新的函数对象,该函数对象只能操作 ten_two.a 这个成员变量。
3.5 总结:
- f 调用时的参数,一定要和
function<>
尖括号中的函数参数类型匹配。 - f 调用时的参数,一定是和bind绑定的参数
std::placeholders::_n
是对应匹配的。 -
- 例如,f 的第一个参数一定匹配的是
std::placeholders::_1
。
- 例如,f 的第一个参数一定匹配的是
-
- 例如,f 的第二个参数一定匹配的是
std::placeholders::_2
。
- 例如,f 的第二个参数一定匹配的是
-
- 例如,f 的第三个参数存在,但是但是bind函数没有
std::placeholders::_3
,但是有其他常量已经绑定,那么f 传参无效,以常量为准。
- 例如,f 的第三个参数存在,但是但是bind函数没有
- bind从第二个参数起,往后的参数一定是与函数func参数顺序是匹配的。
-
- 例如,bind第二个参数,一定匹配的是func的第一个参数。
-
- 例如,bind第三个参数,一定匹配的是func的第二个参数。
-
- 无论bind函数中参数(从第二个参数起)是否占位(常量/
std::placeholders::_n
),都是与func函数中参数对应匹配的。
- 无论bind函数中参数(从第二个参数起)是否占位(常量/
匹配图:
std::bind
函数中的std::placeholders
的个数应该和std::function
尖活号中函数的形参个数一样。
当然了上面匹配图是不一样的,一样也是可以的: