当前位置: 首页 > news >正文

C++修炼:C++11(二)

         Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!

我的博客:<但凡.

我的专栏:《编程之路》、《数据结构与算法之美》、《题海拾贝》、《C++修炼之路》

欢迎点赞,关注!

目录

1、可变参数模板

        1.1、基本语法

        1.2、包扩展

        1.3、emplace系列接口

2、类的新功能

        2.1、默认的移动构造和移动赋值

        2.2、委托构造

        2.3、default和delete


1、可变参数模板

        可变参数模板(Variadic Templates)是C++11引入的一个重要特性,它允许模板接受任意数量和类型的参数。

        1.1、基本语法

        可变参数模板可以使函数模板或者类模板支持任意多个类型的变量。可变数目的参数被称为参数包。存在两种参数包,第一种是模板参数包,第二种是函数参数包。参数包可以接受0个参数。

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

        比如,在上面三个函数模板中,Arges是模板参数包,arges是函数参数包。我们需要注意一下格式,注意一下三个点的位置。

        对于上面的三个函数模板,第三个函数模板我们使用的是右值引用,这意味着对于参数包中的每个类型都是使用的万能引用。如果传左值,这个参数包中的类型被推导为左值引用,引用折叠后为左值引用。如果传右值的话,类型被推导为右值引用,折叠后为右值引用。

        可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。

        我们可以使用sizeof...来计算参数包中参数的个数。

#include<iostream>
using namespace std;template<class ...Args> 
void func(Args... args) 
{cout << sizeof...(args) << endl;
}
int main()
{func();//0func(1);//1func(1, 2);//2func(1, 2,"sss");//3return 0;
}

        这个计算也是编译时计算,因为本质上我们就是实例化出四个函数。其实参数包也是使模板进一步的泛型化。

        本质上是替换了这四个函数:

void func()
{}
void func(int a)
{}
void func(int a,int b)
{}
void func(int a,int b,const char* str)
{}

        1.2、包扩展

        现在我们实现一个print,对于传进函数的每一个变量都打印一次。

         注意参数包不能这样用,因为参数包不是一个容器:

template <class ...Args>
//void Print(Args... args)
//{
// // 可变参数模板编译时解析 
// // 下⾯是运⾏获取和解析,所以不⽀持这样⽤ 
// cout << sizeof...(args) << endl;
// for (size_t i = 0; i < sizeof...(args); i++)
// {
// cout << args[i] << " ";
// }
// cout << endl;
//}

        那么我们怎么实现print函数呢?我们可以使用包扩展来实现。

        包扩展是在编译时进行的。包扩展本质上是递归调用,但是是在编译时递归。

void ShowList()
{// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数 cout << endl;
}
template <class T, class ...Args>
void ShowList(T x, Args... args)
{cout << x << " ";// args是N个参数的参数包 // 调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包 ShowList(args...);
}
// 编译时递归推导解析参数 
template <class ...Args>
void Print(Args... args)
{ShowList(args...);//注意传参的时候三个点又写到后面了
}
int main()
{Print();Print(1);Print(1, "xxxxx");Print(1, "xxxxx", 2.2);return 0;
}

        我们把参数传给print,然后print在调用showlist,每次“剔除”一个变量。知道参数包中变量数为0,此时再去调用showlist直接调用最上面的showlist函数。

        正因为是编译时调用,所以我们不能这样写: 

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

        因为这个函数结束条件是运行时判断逻辑。 

        还有一种包扩展的方式:

template <class T>
const T& GetArg(const T& x)
{cout << x << " ";return x;
}
template <class ...Args>
void Arguments(Args... args)
{cout << endl;
}
template <class ...Args>
void Print(Args... args)
{// 注意GetArg必须返回或者到的对象,这样才能组成参数包给Arguments Arguments(GetArg(args)...);
}

        我们把传入print的几个参数组成参数包传给GetArg。对于GetArg来说,我们相当于把参数包中的每个参数都传给GetArg,然后把GetArg的所有返回值在组成一个参数包,传给Arguments. 这种包扩展就是普通的编译推导,并不是递归。

        需要注意的是上面这种的包扩展方式在vs上是倒序输出的,也就是说输出结果和我们的传参顺序恰好相反。造成这种结果的原因是C++标准没有规定函数参数的求值顺序,而大部分编译器默认是从右到左。

        1.3、emplace系列接口

        C++11之后所有的stl容器都新增了emplace接口,empalce系列的接口均为模板可变参数。接下来我们使用list来测试一下emplace系列的接口。

         

        对于emplace_back和push_back来说,无论是传左值,还是传右值,效率都没有区别。唯一有区别的场景就是这种:

int main()
{list<string> li;li.push_back("sss");//构造加移动构造li.emplace_back("sss");//直接构造return 0;
}

        对于push_back来说,因为push_back这个函数的形参就是string&&类型的,如果我们想让形参和实参匹配上,需要先对形参进行构造,构造一个临时对象,然后再移动构造给string对象进行push_back。

        但是对于emplace_back来说,由于他是一个模板,他可以直接接受const char*类型的变量,然后再在插入之前进行一次构造,构造出string对象进行插入就可以了。

         接下来我们看下面这个场景:

#include<iostream>
#include<list>
#include<string>
using namespace std;
int main()
{list<pair<int,double>> li;li.push_back({ 6,5.5 });//li.emplace_back({ 6,5.5 });li.emplace_back(6, 5.5);return 0;
}

         对于pair类型,我们使用emplace接口时不能传花括号,也就是说不能传初始化列表。因为咱们的emplace_back是模板函数,所以在传参是他会去推导类型。由于initializer_list在推导时必须是initializer_list<T>,也就是说列表中的值类型必须是相同的。但是这里一个int,一个double不是同一类型参数,所以说会编译报错。

        但是对于直接传构造底层对象的参数是没有问题的。这也是emplace系列接口的正确用法:在插入值时向emplace系列接口中直接传入构造底层存储对象类型所需的参数。这样相比普通插入效率更高。但如果容器存储的是一些基本类型(如int,double,char)时使用emplace_back或push_back效率上没有差异。

        现在我们对之前模拟实现的list进行一下升级,写一下emplace系列接口:

......
list_node(T&& x):_next(nullptr), _prev(nullptr), _data(std::move(x))
{}
//如果上面两个构造函数都给了默认值=T(),那么当new node时,也就是不传参是无法确定匹配哪个构造函数。
//一般右值版本不给默认值。右值引用通常用于移动语义,而默认构造的临时对象(T())不适合被移动。
// 语义上矛盾:右值引用表示要"窃取"资源,但默认参数会创建一个新对象
template<class ...Args>
list_node(Args&&... args): _next(nullptr), _prev(nullptr), _data(std::forward<Args>(args)...)
{}
......
template<class ...Args>
void emplace_back(Args&&... args)
{emplace(end(), std::forward<Args>(args)...);
}
template<class ...Args>
void emplace(iterator pos, Args&&... args)
{Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(std::forward<Args>(args)...);// prev newnode curprev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;
}

         注意的是我们的参数包构造不需要解析参数包,也就是说编译器不需要进行包扩展什么的,编译器不需要一个一个看都是什么类型,而是直接拿去和list底层存储类型的构造函数匹配。如果匹配对不上就报错。

        另外就是每次传参数包我们都需要完美转发。让参数包中的参数都保持原有属性。                 

2、类的新功能

        2.1、默认的移动构造和移动赋值

        C++11新增了两个默认成员函数:移动构造函数和移动赋值运算符重载。

        如果你没有自己生成移动构造函数,且没有实现析构函数,拷贝构造,拷贝赋值重载中的任意一个,那么编译器就会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。

        同理,如果你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会 执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。

        如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

        2.2、委托构造

class A
{
public:A(int a, int b):_a(a),_b(b){}A(int a, int b, char c):A(a, b){_c = c;}
private:int _a;int _b;char _c=0;
};

        上面这个类就实现了委托构造,本质上也是一种复用。 

        2.3、default和delete

        default可以强制让编译器生成默认函数:

class MyClass {
public:MyClass() = default; // 显式要求编译器生成默认构造函数MyClass(const MyClass&) = default; // 默认拷贝构造函数
};

        delete就与default相反,不让编译器生成某个默认函数:

class NonCopyable {
public:NonCopyable(const NonCopyable&) = delete;NonCopyable& operator=(const NonCopyable&) = delete;
};

         好了,今天的内容就分享到这,我们下期再见!

相关文章:

  • 鸿蒙仓颉语言开发实战教程:商城应用个人中心页面
  • 数 据 结 构 进 阶:哨 兵 位 的 头 结 点 如 何 简 化 链 表 操 作
  • conda环境配置(二) —— 报错
  • Macbook M3 使用 VMware Fusion 安装 openEuler24.03LTS
  • 性能测试-jmeter实战2
  • ​React Hooks 的闭包陷阱问题
  • 【看到哪里写到哪里】C的“数组指针”
  • 宝塔安装配置FRP
  • 【第七篇】 SpringBoot项目的热部署
  • 基于SpringBoot解决RabbitMQ消息丢失问题
  • 嵌入:AI 的翻译器
  • 64、js 中require和import有何区别?
  • 解决MySQL8.4报错ERROR 1524 (HY000): Plugin ‘mysql_native_password‘ is not loaded
  • 深入理解 Agent 与 LLM 的区别:从智能体到语言模型
  • 为什么React列表项需要key?(React key)(稳定的唯一标识key有助于React虚拟DOM优化重绘大型列表)
  • 接口不是json的内容能用Jsonpath获取吗,如果不能,我们选用什么方法处理呢?
  • K8S认证|CKS题库+答案| 5.日志审计
  • 动端React表格组件:支持合并
  • ThinkPHP 5.1 中的 error 和 success 方法详解
  • Java高级 | 【实验七】Springboot 过滤器和拦截器
  • 海南建设网站/网页制作教程步骤
  • 网站建设属于设备吗/网络推广网站的方法
  • 网站建设计划书/八上数学优化设计答案
  • 低价郑州网站建设/网站如何推广
  • 手机适配网站/企业seo优化服务
  • 网站建设设计报价/网推团队