【C++】C++11新特性详解:可变参数模板与emplace系列的应用

C++语法 相关知识点 可以通过点击 以下链接进行学习 一起加油!
命名空间 缺省参数与函数重载 C++相关特性 类和对象-上篇 类和对象-中篇
类和对象-下篇 日期类 C/C++内存管理 模板初阶 String使用
String模拟实现 Vector使用及其模拟实现 List使用及其模拟实现 容器适配器Stack与Queue Priority Queue与仿函数
模板进阶-模板特化 面向对象三大特性-继承机制 面向对象三大特性-多态机制 STL 树形结构容器 二叉搜索树
AVL树 红黑树 红黑树封装map/set 哈希-开篇 闭散列-模拟实现哈希
哈希桶-模拟实现哈希 哈希表封装 unordered_map 和 unordered_set C++11 新特性:序章 右值引用、移动语义、万能引用实现完美转发

大家好,我是店小二。在这篇文章中,我们将深入探讨C++11的新特性------可变参数模板和emplace系列的应用。如果在阅读过程中有不清楚的地方或发现任何错误,欢迎随时私信交流探讨。


🌈个人主页:是店小二呀

🌈C语言专栏:C语言

🌈C++专栏: C++

🌈初阶数据结构专栏: 初阶数据结构

🌈高阶数据结构专栏: 高阶数据结构

🌈Linux专栏: Linux

🌈喜欢的诗句:无人扶我青云志 我自踏雪至山巅

文章目录

一、新的类功能

在C++11前,C++类有六个默认成员函数(默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数 )

1.1 移动语义注意项

C++11 新增了两个:移动构造函数和移动赋值运算符重载 。在关于右值引用篇章有相关介绍:右值引用与移动语义

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

1.2 Rule of Five机制

C++11 引入了五个特殊成员函数,其中有三对:

  1. 拷贝构造函数拷贝赋值运算符
  2. 移动构造函数移动赋值运算符
  3. 析构函数

当你定义其中的一个(如移动构造函数),编译器会认为你对这个类的资源管理有特殊的要求 ,因此不再生成默认的拷贝构造函数和拷贝赋值运算符,避免误用浅拷贝导致资源管理错误

析构函数和移动构造函数的不同角色:

  • 析构函数 :用于销毁对象并释放其占用的资源。显式定义析构函数意味着你要自行控制资源的释放方式。C++ 假定你手动管理资源,因此不会为你生成其他依赖于默认资源管理的函数(如移动构造函数)
  • 移动构造函数:用于将资源从一个对象转移到另一个对象。它不负责销毁对象,而是将对象的资源"转交"给另一个对象。

移动构造函数主要是负责"转移"资源,而不是释放资源,编译器假设转移资源并不改变析构时的行为,所以它会继续生成默认析构函数,认为默认的资源释放机制(如自动销毁对象的成员)依然有效。对此当显示实现移动构造函数,编译器也会自动生成默认的析构函数,确保资源的销毁。

1.3 小结

析构函数、拷贝构造、拷贝赋值重载是对于容器中深拷贝的类关于资源的管理,为了避免潜在的资源管理问题和不一致性 ,当你显式定义了析构函数时,编译器会尊重你的选择,不再生成默认的移动构造函数,需要你根据具体的类设计和资源管理策略,决定是否需要自定义移动构造函数(这三个特殊成员函数之间存在依赖的关系Rule of Five机制)。

cpp 复制代码
// 以下代码在vs2013中不能体现,在vs2019下才能演示体现上面的特性。
class Person
{
    public:
    Person(const char* name = "", int age = 0)
        :_name(name)
            , _age(age)
        {}
    /*Person(const Person& p)
:_name(p._name)
,_age(p._age)
{}*/
    /*Person& operator=(const Person& p)
{
if(this != &p)
{
_name = p._name;
_age = p._age;
}
return *this;
}*/
    /*~Person()
{}*/
    private:
    //自定义成员
    bit::string _name;
    //内置类型成员
    int _age;
};
int main()
{
    Person s1;
    Person s2 = s1;
    Person s3 = std::move(s1);
    Person s4;
    s4 = std::move(s2);
    return 0;
}

需要注意,关于移动语义是一种夺舍的行为,需要考虑被夺舍对象是否需要使用原本的资源,进行调用。

二、"强制生成"默认函数的关键字default

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成

特殊函数之间可能存在依赖或互斥的关系,编译器不会随意地插入可能与用户意图不符的代码,也就导致了当强制生成移动语句,编译器不会默认生成析构函数等与之依赖性强的函数,如果需要移动语句和拷贝函数等函数同时出现,建议全部进行强制生成。

因为自己去写的话,还是麻烦了一点,关于这些问题可以看成一个语法规定就好了。

四、"禁止生成"默认函数的关键字delete

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

如以下场景,这里类只能在堆上生成对象

cpp 复制代码
class HeapOnly
{
    public:
    static HeapOnly* CreateObj()
    {
        return new HeapOnly;
    }

    //C++11
    HeapOnly(const HeapOnly&) = delete;

    //C++98 私有+只声明不实现
    private:
    HeapOnly(const HeapOnly&);

    HeapOnly()
    {}

    int _a = 1;
};
int main()
{
    //HeapOnly ho1;
    //HeapOnly* p1 = new HeapOnly;
    //以上是构造函数私有
    
    HeapOnly* p2 = HeapOnly::CreateObj();
	//尝试在堆上开辟空间
    
    // 不能被拷贝,才能禁止
    //HeapOnly obj(*p2);

    return 0;
}

分析几行代码:

  1. HeapOnly* p1 = new HeapOnly;这里构造函数是私有的
  2. HeapOnly obj(*p2);不能被拷贝,禁止拷贝构造函数,也是栈上开空间
  3. HeapOnly* p2 = HeapOnly::CreateObj();通过静态工厂方法在堆上创建对象

三、可变参数模板

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。

3.1 基本可变参数的函数模板

cpp 复制代码
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。(可以自动推导类型)
template <class ...Args>
    void ShowList(Args... args)
{}

3.2 获得参数包的值

3.2.1 不支持使用args[i]

参数args前面有省略号,所以它就是一个可变模板参数;将带有省略号的参数称为参数包,它里面包含了0到N(N >= 0)个模板参数。

我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值

cpp 复制代码
template<class ...Args>
    void Cpp_Printf(Args... args)
{

    cout << sizeof...(args) << endl;

    // error C3520: "args": 必须在此上下文中扩展参数包
    // 不支持
    for (size_t i = 0; i < sizeof...(args); i++)
    {
        cout << args[i] << endl;
    }
    cout << endl;
}

int main()
{
    Cpp_Printf(1,'A',"sort");
    return 0;
}

使用 args[i] 这样的写法在编译时会导致错误,因为模板参数包 args 并不是一个数组,不能使用索引访问。

3.2.2 递归函数方式展开参数包

**参数包中的参数类型确定需要在编译时确定,**这意味着不能在运行时动态推断参数包中每个参数的具体类型,而在递归中模板推导参数类型是在编译时进行的。

cpp 复制代码
void ShowList ()
{
    cout << endl;
}

template <class T, class ...Args>
    void ShowList (T& value, Args... args)
{
    cout << value << " ";
    ShowList(args...);
}

int main()
{
    ShowList(1);
    ShowList(1, 'A');
    ShowList(1, 'A', std::string("sort"));
    return 0;
}

在调用函数时,第一个参数必须能够匹配到 T 类型Args... args:这是一个参数包,用来接收除了第一个参数 value 之外的所有剩余参数。因此可以通过模板的递归展开,每次处理一个参数,并递归地处理剩余的参数,知道没有参数需要处理为止。

3.2.3 逗号表达式展开参数包

这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式

实现的关键是逗号表达式。

具体说明:

  1. 这里主要是利用了参数包展开的特性及其列表初始化会进行遍历初始化。这里(PrintArg(args), 0),按照顺序执行逗号表达式,先执行PrintArg(args),返回结果为0,用于数组元素存储。
  2. 通过列表初始化特性, {(printarg(args), 0)...}将会展开成((printarg(arg1),0),(printarg(arg2),0),(printarg(arg3),0), etc... ),最终会创建一个元素值都为0(逗号表达式,取最后的值)的数组int arr[sizeof... (Args)]
  3. 由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包
cpp 复制代码
template <class T>
    void PrintArg(T t)
{
    cout << t << " ";
}
//展开函数
template <class ...Args>
    void ShowList(Args... args)
{
    int arr[] = { (PrintArg(args), 0)... };
    cout << endl;
}
int main()
{
    ShowList(1);
    ShowList(1, 'A');
    ShowList(1, 'A', std::string("sort"));
    return 0;
}

四、emplace系列(尽量配合参数包)

STL容器(string不支持)中emplace相关接口函数:

cpp 复制代码
template <class... Args>
    void emplace_back(Args&&... args)

emplace系列的接口支持模板的可变参数和万能引用。

4.1 empalce系统的优势

那么相对于insert和emplace系列接口的优势到底在哪里呢?

乍一看无论是使用左值还是右值,感觉insert和emplace没啥区别啊!这里没有体现出可变参数包的作用,那么再通过一个例子就行更加深入了解。

如果是emplace_back还是单纯的同push_back传递pair对象,那么也没有多大差别。如果是按照蓝色框框传给参数包或直接传递参数,那么emplace系列作用得以体现。注意这里模板推导出来,不要将模板和模板推导函数混在一起。


直接传递pair的参数包,参数包一直往下传,底层直接构造。这里建议大家使用emplace系列更加高效(不一定高效,需要分场合)。

4.2 emplace使用推荐

emplace系列函数在C++中用于在容器构造对象,而不是拷贝现有对象。它们通常与可变参数模板一起使用,以便于直接在容器内部就地拷贝对象,而不是通过拷贝构造函数或移动构造函数进行操作

个人理解:emplace传左值,在传参过程中会调用拷贝构造,对于右值,万能引用会推出右值,使用右值引用接收,没有拷贝构造的调用

  • 直接emplace 或 push/insert 左值 ---> 构造 + 移动构造

  • 直接emplace 参数包---> 构造

  • 有移动构造深拷贝对象,差别不大,由于移动构造的代价很小

  • 直接emplace 或 push/insert 右值/浅拷贝右值对象 ---> 构造 + 拷贝构造

  • 直接emplace 参数包---> 构造

  • 代价就大了很多,由于拷贝构造代价很大,没有移动构造浅拷贝的对象,区别也比较大

对此以后使用容器接入接口,推荐emplace系列,push系列/insert的接口,推荐使用emplace系列代替,其次emplace能用参数包就用参数包。就是构造函数中_data(s1)和 _data("11")会导致什么后果。


以上就是本篇文章的所有内容,在此感谢大家的观看!这里是店小二呀C++笔记,希望对你在学习C++语言旅途中有所帮助!

相关推荐
zhoujun79817 分钟前
Hybrid APP 移动开发-客户端
android
重生之我是数学王子22 分钟前
QT简易项目 数据库可视化界面 数据库编程SQLITE QT5.12.3环境 C++实现
数据库·c++·qt
Darkwanderor25 分钟前
迄今为止的排序算法总结
数据结构·c++·算法·排序算法
for_syq29 分钟前
Android res复制脚本
android·linux·服务器
Want59533 分钟前
C/C++绘制爱心
c语言·开发语言·c++
黑不溜秋的40 分钟前
C++ 编程指南06 - 不要泄漏任何资源
c++
TANGLONG2221 小时前
【初阶数据结构和算法】leetcode刷题之设计循环队列
java·c语言·数据结构·c++·python·算法·leetcode
OTWOL2 小时前
零基础学指针(下)
c语言·c++
bigbig猩猩2 小时前
C/C++链接数据库(MySQL)超级详细指南
c语言·数据库·c++
WeiComp2 小时前
Android数据存储——文件存储、SharedPreferences、SQLite、Litepal
android·数据库·sqlite·litepal