【C++11】新的类功能、模板的可变参数、包装器
📚 博主的专栏
🐧 Linux | 🖥️ C++ | 📊 数据结构 | 💡C++ 算法 | 🌐 C 语言
上篇文章:列表初始化、右值引用、完美转发、lambda表达式
下篇文章:Linux,线程id,线程互斥
本篇文章主要讲解:
- 新的类功能
- 模板的可变参数
- 包装器
目录
新的类功能
默认成员函数
类成员变量初始化
强制生成默认函数的关键字default:
禁止生成默认函数的关键字delete:
模板的可变参数
模版可变参数的用法一:
用法二:
emplace:
emplace的使用场景:
实现一个emplace系列:
C++11中的包装器:std::function与std::bind
std::function:多态函数包装器
支持的可调用对象类型:
应用场景:
bind
1. 基本用法
2. 绑定成员函数
3. 绑定成员变量
4. 参数绑定与引用
5. 与 std::function 结合
6. 对比 Lambda 表达式
实际应用场景:
何时使用 std::bind?
新的类功能
默认成员函数
原来C++类中,有6个默认成员函数:
1. 构造函数
2. 析构函数
3. 拷贝构造函数
4. 拷贝赋值重载
5. 取地址重载
6. const 取地址重载
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个:移动构造函数和移动赋值运算符重载。也就是上篇文章右值引用所详细讲解的。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,
如果实现了就调用移动构造,没有实现就调用拷贝构造。
如图:我只写了Person的构造函数,满足以上要求,s3调用了默认的移动构造,取走了s1的资源
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
移动赋值也是同样,s4调用移动赋值。
类成员变量初始化
C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化,这个我们在类和对象默认就讲了,这里就不再细讲了
强制生成默认函数的关键字default:
这个我们也在之前的文章讲解用到过
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。但是会导致拷贝构造受到影响
因此我们强制生成移动赋值:又会导致赋值重载受到影响,因此在使用default的时候要注意要解决会牵扯到的连锁反应。
禁止生成默认函数的关键字delete:
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
如果不期望某个类被拷贝
只期望这个类只能在堆上生成对象,普通情况下,我可以随意创建对象,但是如果把构造函数私有化 ,如何实现?
因此我们将这个CreateObj成为静态的,就没有this指针,就不用对象调用,直接使用类域
调用拷贝构造,让这个新的对象obj仍然在栈上。
HeapOnly obj(*p2); //obj还是栈上的对象
C++98的解决办法:私有加只声明不实现:
C++11:这个函数后加delete,称=delete修饰的函数为删除函数,,与default是相反的关键字
HeapOnly(const HeapOnly&) = delete;
继承和多态中的final与override关键字
这个我们在继承和多态章节文章已经进行了详细讲解这里就不再细讲。
模板的可变参数
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,现阶段,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们点到为止。
下面就是一个基本可变参数的函数模板
Args(arguments-参数)是一个模板参数包,args是一个函数形参参数包
声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数
template <class ...Args>
void ShowList(Args... args)
{
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值
模版可变参数的用法一:
为什么不支持?
因为args[i]是一个运行时程序,解析这参数包的东西的时候是在编译时逻辑,两者不匹配。
使用一个空函数,当参数传完之后。会调用到这个空函数,就结束
这是一个编译时,参数推导递归。
void _Cpp_Printf() {cout << endl; }
我们写的模版写给编译器,编译器用我们的模版生成代码。
编译器生成的代码,大致就是这样的,所谓的参数包还是要推演成实际的参数,这张图是编译器的推导过程:
注意:对于编译器而言,我们写的模版的T,对于编译器就是实际的类型,而参数包对于编译器也一样,也是实际的参数。
用法二:
采用逗号表达式,让编译器在编译的时候自动去推导;编译时推导,args...参数有几个值,PrintArg就调用几次,就有几个返回值,arr就开多大。
template <class T>
int PrintArg(T t)
{cout << t << " ";return 0;
}template <class ...Args>
void Cpp_Printf(Args... args)
{// 编译时推导,args...参数有几个值,PrintArg就调用几次,就有几个返回值,arr就开多大int arr[] = { PrintArg(args)... };cout << endl;
}int main()
{/*Cpp_Printf(1.1);Cpp_Printf(1.1, 'x');*/Cpp_Printf(1, 'A', std::string("sort"));return 0;
}
在底层的角度,编译器推导就是这样的
void Cpp_Printf(int x, char y, std::string z)
{int arr[] = { PrintArg(x),PrintArg(y),PrintArg(z) };cout << endl;
}
也就是这样:
推荐把引用加上,也就是万能引用,传左值就得左值,传右值就得右值。
实际示例:STL容器中的emplace相关接口函数
emplace:
template <class... Args>
void emplace_back (Args&&... args);
建议使用emplace,因为emplace某些情况比push_back更加高效
在这种情况下面用emplace和push_back没什么区别
emplace的使用场景:
直接传pair对象的参数包,参数包一直往下传,最后把参数包直接拿去构造,直接只构造一次,争对浅拷贝的效率提升更高,而push_back就需要传pair。
实现一个emplace系列:
在模拟实现的List当中:注意要使用完美转发,因为右值引用属性降为了左值
int main() {// 没区别list<pupu::string> lt;pupu::string s1("xxxxxxxxxxxx");lt.push_back(s1);lt.push_back(move(s1));cout << endl;pupu::string s2("xxxxxxxxxxxx");lt.emplace_back(s2);lt.emplace_back(move(s2));return 0; }
运行结果:与上方库中实现的相同,
实际上,编译器就将这个参数包实例化:
和push_back的主要区别还是可以传参数包。
展开编译器的推导过程
总结:
尽可能避免使用左值:
C++11中的包装器:std::function
与std::bind
C++11引入了两个重要的工具——std::function
和std::bind
,用于更灵活地管理可调用对象(如函数、Lambda、成员函数等)。它们统称为函数包装器,旨在统一不同类型的可调用对象,提升代码的通用性和可维护性。
std::function
:多态函数包装器
std::function
是一个模板类,可以存储、复制和调用任何可调用对象(函数、Lambda、绑定表达式等)。其核心作用是类型擦除,允许用户以统一的方式操作不同类型的可调用实体。底层就是仿函数。
支持的可调用对象类型:
普通函数、函数指针:如
int func(int, int) -->类型定义复杂
成员函数:需结合
std::bind
或Lambda函数对象(仿函数):重载了
operator()
的类实例 ---> 要定义一个类,用的时候有点麻烦,不适合同一类型Lambda表达式 ----> 没有类型概念
function不是定义可调用对象,而是包装可调用对象
语法格式:function<可调用对象的返回值(可调用对象的参数)> 对象名;
//function<可调用对象的返回值(可调用对象的参数)>function<int(int, int)> fc1;
基本用法:改调用什么就掉用什么,只是包装起来,看起来规范统一
#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()
{ //function<可调用对象的返回值(可调用对象的参数)>function<int(int, int)> fc1;//function<int(int, int)> fc2(f);function<int(int, int)> fc2 = f;function<int(int, int)> fc3 = Functor();function<int(int, int)> fc4 = [](int x, int y) { return x + y; };cout << fc2.operator()(1, 2) << endl;cout << fc3(1, 2) << endl;cout << fc4(1, 2) << endl;return 0;
}
例题:150. 逆波兰表达式求值 - 力扣(LeetCode)
这是之前的解法:
class Solution {
public:int evalRPN(vector<string>& tokens) {stack<int> st;//initializer_list构造函数set<string> s = {"+","-","*","/"};for(auto str : tokens) {//1.操作数入栈,操作符运算if(s.find(str) != s.end()){//操作符int right = st.top();st.pop();int left = st.top();st.pop();switch(str[0])//case必须是整型(char也是整型){case '+':st.push(left + right);break;case '-':st.push(left - right);break;case '*':st.push(left * right);break;case '/':st.push(left / right);break;}}else{//没有找到操作符,就入栈st.push(stoi(str));//string转化为int类型}}return st.top();}
};
现在可以这样写:想要加其他的运算符,只用在上面添加方法,下面的整体逻辑都不用改
class Solution {
public:int evalRPN(vector<string>& tokens) {stack<int> st;// initializer_list构造函数set<string> s = {"+", "-", "*", "/"};// 命令 -> 动作(函数)map<string, function<int(int, int)>> opFuncMap = {{"+", [](int a, int b) { return a + b; }},{"-", [](int a, int b) { return a - b; }},{"*", [](int a, int b) { return a * b; }},{"/", [](int a, int b) { return a / b; }}};for (auto& str : tokens) {if (opFuncMap.count(str)) // 返回1就是操作符{// function<int(int, int)> func = opFuncMap[str];int right = st.top();st.pop();int left = st.top();st.pop();st.push(opFuncMap[str](left, right));} else // 返回操作数,操作数入栈{st.push(stoi(str));}}return st.top();}
};
代码关键点:
1.map::count(key)
- 返回
1
:表示当前字符串str
是map中的一个键,即str
是操作符(+
,-
,*
,/
中的一个)。- 返回
0
:表示str
不是操作符,应视为操作数。2.结合map,使用function包装将运算符和对应的操作组装在一起,只用在后面去识别操作符执行对应的操作就能完成任务。
对比:
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;// lambda表达式cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;cout << endl << endl;return 0;
}
这会导致对象实例化三次:没有同一类型
使用包装器之后:对象只实例化了一次,在类型上进行了统一
包装的是静态成员函数指针,要注意:加类域
// 包装成员函数指针
class Plus
{
public:static int plusi(int a, int b){return a + b;}double plusd(double a, double b){return a + b;}
};int main()
{// 成员函数的函数指针 &类型::函数名function<int(int, int)> fc1 = &Plus::plusi;cout << fc1(1, 2) << endl;return 0;
}
包装非静态成员函数
C++11语法规定,成员函数,函数名不仅要指定类域还要在前面加一个取地址符号,静态可以不加,建议加上:这里仍然会报错。
这是因为普通成员函数,第一个参数是隐藏的对象指针,在写包装器要将参数写全
function<double(Plus*, double, double)> fc2 = &Plus::plusd;
Plus plus;
cout << fc2(&plus, 1.1, 2.2) << endl;
也可以直接传匿名对象。
function<double(Plus, double, double)> fc3 = &Plus::plusd;cout << fc3(Plus(), 1.1, 2.2) << endl;
应用场景:
回调机制:将不同来源的回调函数统一存储。
策略模式:动态替换算法实现。
事件处理:管理事件监听器。
bind
std::bind函数定义在头文件中,是一个函数模板(function是一个类模版),它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。
简单来讲:绑定是用来调整可调用对象的参数个数或者顺序
原型:没有返回值
// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
- 可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
- 调用bind的一般形式:auto newCallable = bind(callable,arg_list);
- 其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
- arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示
- newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。
1. 基本用法
头文件和命名空间:
#include <functional>
using namespace std::placeholders; // 简化占位符(_1, _2...)
见一见:
输出 :
5
-5
查看类型:
实际上binder也是一个仿函数,bind的底层也是仿函数,返回的是一个仿函数对象
如图:
调整参数顺序意义不大,调整参数个数意义比较大 。
2. 绑定成员函数
绑定成员函数时,第一个参数需是对象实例(指针、引用或对象本身):把固定参数绑定死(比如对象和指针被绑死固定,在传参的时候只用传两个可变参数,就可以做到是用这个对象还是指针调用这个函数)
应用场景:
打印英雄血量和蓝量:以前这样写
void fx(const string& name, int x, int y) {cout << name << "->[" << "血量:" << x << ",蓝:" << y << ']' << endl; }int main() {fx("王昭君", 80, 20);fx("王昭君", 85, 10);fx("王昭君", 99, 0);fx("王昭君", 99, 80);fx("亚瑟", 99, 80);fx("亚瑟", 91, 80);fx("亚瑟", 5, 80);return 0; }
运行结果:
每次都要写王昭君和亚瑟很烦,因此我们想写一个能将他们绑定上的方法:
auto f6 = bind(fx, "王昭君", placeholders::_1, placeholders::_2);f6(80, 20); f6(85, 10); f6(99, 0); f6(99, 80);auto f7 = bind(fx, "亚瑟", placeholders::_1, placeholders::_2);f7(80, 20); f7(85, 10); f7(99, 0); f7(99, 80);
原理:
绑定不是只能绑定死一个,想绑定几个就绑定几个,_1永远表示第一个实参,_2永远表示第二个实参
3. 绑定成员变量
auto f = bind(&Foo::data, _1);
cout << f(obj) << endl; // 输出 42
4. 参数绑定与引用
默认按值传递参数,使用 std::ref
传递引用:
void modify(int& x) { x++; }int main() {int val = 10;auto f = bind(modify, std::ref(val));f(); // val 变为 11
}
5. 与 std::function
结合
绑定后的对象类型未指定,通常用 std::function
包装:
std::function<void(int, int)> callback = bind(print, _1, _2);
callback(1, 2); // 输出 "1, 2"
绑定的东西除了传给模版和auto还能传给function
function<void(std::string, int)> f8 = bind(fx, placeholders::_1, 80, placeholders::_2);
f8("武则天", 50);
f8("韩信", 40);cout << typeid(f7).name() << endl;
cout << typeid(f8).name() << endl;
类型就变成function,而不是bunder了
6. 对比 Lambda 表达式
std::bind
的某些场景可用 Lambda 替代,通常更直观:
// 用 Lambda 绑定参数
auto lambda = [](int a, int b) { print(a, b); };
lambda(3, 4);// 绑定成员函数的 Lambda 写法
auto f_lambda = [&obj](int x) { obj.bar(x); };
f_lambda(300);
实际应用场景:
假设这个-十分灵活,能乘一个我构造对象的时候传的参数
万一一个方法对应的参数不是两个呢,是三个,我也想让他写在这里:利用绑定帮助修改参数个数
map<string, function<int(int, int)>> opFuncMap = {{"+", [](int a, int b) {return a + b; }},{"-", bind(&Sub::sub, Sub(10), placeholders::_1, placeholders::_2)},{"*", [](int a, int b) {return a * b; }},{"/", [](int a, int b) {return a / b; }}
};
一般情况下函数模版可以显示实例化 :
怎么调用fy
显示实例化:
当我使用bind的时候想要指定返回值,就可以显示实例化给这个result参数
何时使用 std::bind
?
需要复用已有函数逻辑,调整参数顺序/数量。
与旧代码或接口(如回调函数)兼容时。
但现代 C++ 更推荐优先使用 Lambda(可读性更好,支持捕获局部变量)。
结语:
随着这篇关于题目解析的博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。
在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。
你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容,让我们在知识的道路上共同前行。