目录
一,新的类功能
1-1,默认成员函数
1-2,强制生成关键字
二,可变参数模板
2-1,模板参数包
2-2,STL容器empalce的相关接口
一,新的类功能
1-1,默认成员函数
C++11之前的类中有6个默认成员函数(默认成员函数就是我们不写编译器会生成一个默认的):1. 构造函数。2. 析构函数。3. 拷贝构造函数。4. 拷贝赋值重载。5. 取地址重载。6. const 取地址重载。这六个之中重要的是前4个,后两个取地址的用处不大。
C++11新增了两个:移动构造函数和移动赋值运算符重载。此两种默认成员函数与其它的默认成员函数有所不同,需说明以下三点。
1,如果你没有自己实现移动构造函数,且没有自己实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,即浅拷贝。自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
2,如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,即浅拷贝。自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
3,如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值,这时需自己实现拷贝构造和拷贝赋值。因为当你自己编写移动构造或移动赋值后,说明你可能想要完全控制对象的拷贝行为,且有了移动语义通常也需要深拷贝,而默认的拷贝操作都是浅拷贝,所以为了确保对象的拷贝行为符合预期,有了移动语义后编译器不会默认生成拷贝构造和拷贝赋值。
这里的代码演示需借助类结构演示,我们先简单设计String类,如下:
#include <cstring>
#include <iostream>
using namespace std;
namespace bit
{
class string
{
public:
//构造函数
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
//赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
//移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
//移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
//析构函数
~string()
{
delete[] _str;
_str = nullptr;
}
private:
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
char* _str;
size_t _size;
size_t _capacity;
};
}
上面string类中自己实现了移动构造和移动赋值,编译器不会默认生成拷贝构造和拷贝赋值,若这里不自己实现拷贝构造和拷贝赋值,将不能实现有关拷贝构造和拷贝赋值的语句。下面来用以上类观察默认移动构造和默认移动赋值。
#include "String.h" //设计string的头文件
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name) , _age(age)
{ }
private:
bit::string _name;
int _age;
};/*自定义string类中实现了移动语义,不会生成默认拷贝构造和默认拷贝赋值,若自己不实现的话这里将会出错*/
int main()
{
Person s1;
cout << endl;
//拷贝构造
Person s2 = s1;
cout << endl;
//移动构造/*Preson满足默认移动构造的所有条件,它的自定义成员_name将会调用_name自己的移动构造,若没有移动构造将调用拷贝构造*/
Person s3 = move(s1);
cout << endl;
Person s4;
//移动赋值/*与移动构造同理,若定义了_name的移动赋值将调用它移动赋值,若没有定义将调用它的拷贝赋值*/
s4 = move(s2);
cout << endl;
return 0;
}
若在以上Person类中实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,将会输出不一样的结果。
class Person
{//.......
~Person() { } //在以上代码上加上了析构函数//.......
};//main函数都一样
这里可观察到默认移动语义比其它默认函数的条件更为苛刻,需满足两个条件:1,没有自己实现移动语义。2,没有自己实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。
第一个条件好理解,第二个条件就不知所措了。其实这里之所以要这样设计还是跟析构函数 、拷贝构造、拷贝赋值重载三者的作用有关。系统默认生成的它们三个函数都是浅拷贝,当我们自己实现时说明需要深拷贝,这时说明我们需要控制对象的拷贝复制和移动行为,因此这里就不再默认生成,若有需要可自己实现。
1-2,强制生成关键字
强制生成默认函数的关键字default:
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
#include "String.h" //头文件
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name) , _age(age)
{ }~Person() { }
//上面自己编写了析构函数将不会有默认移动语义,这里强制生成默认移动语义
Person(Person&& p) = default;
Person& operator=(Person && p) = default;//上面强制生成了移动语义将不会有默认拷贝和默认赋值,这里强制生成默认拷贝和默认赋值
Person(const Person& p) = default;
Person& operator=(const Person& p) = default;
private:
bit::string _name;
int _age;
};
int main()
{
//默认构造
Person s1;
cout << endl;//拷贝构造
Person s2 = s1;
cout << endl;//拷贝赋值
s1 = s2;
cout << endl;
//移动构造
Person s3 = move(s1);
cout << endl;
//移动赋值
Person s4;
s4 = move(s1);
cout << endl;return 0;
}
禁止生成默认函数的关键字delete:
有些类有时我们可能不希望有个别的默认函数,如拷贝构造、拷贝赋值等。如果想要限制这些默认函数的生成,C++98的做法是自己只声明该函数,且将该函数设置成private(防止别人在类外实现),这样就可做到万无一失,只要调用就会报错。C++11新增关键字delete对此做出处理,只需在该函数声明加上 “=delete” 即可,该语法指示编译器不生成对应函数的默认版本。“=delete”修饰的函数为删除函数。
下面以禁止Person类进行拷贝和赋值为例。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name), _age(age)
{ }~Person() { };
//C++11的做法,delete关键字直接解决
Person(Person&& p) = delete;
Person& operator=(Person && p) = delete;
private:
//以下是C++98的做法,只声明不实现,且放到私有
//Person(const Person& p);
//Person& operator=(const Person & p);
bit::string _name;
int _age;
};
二,可变参数模板
2-1,模板参数包
C++11的新特性可变参数模板能够让我们创建可以接受任意数量和类型的模板包。模板参数包是将传入任意数量和类型的模板参数打包形成的一个整体。相比 C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变参数模版无疑是一个巨大的改进。可变参数模板的知识运用比较广泛,这里不纠结过于复杂的结构,掌握一些基础的可变参数模板特性就足够使用了。
下面就是一个基本可变参数的函数模板,并尝试使用sizeof来输出参数个数。具体细节和说明如下:
/*Args是一个模板参数包,args是一个函数形参参数包。这里可对比template<class Args> void func(Args args);运用时将传入的模板参数打包,形成Args,然后通过Args参数包中的类型定义具有Args包中模板参数类型的函数形参,将其打包形成args函数参数包*/
template <class... Args> //模板参数包Args,具有0到N个类型的模板参数
void func(Args... args) //函数参数包args,具有0到N个类型的函数参数
{
cout << sizeof...(Args) << " "; //输出模板参数包所包含的参数数量
cout << sizeof...(args) << endl; //输出函数参数包所包含的参数数量(它们实际上大小是一样的)
/*注意: 无法使用sizeof(Args或args),因为都是参数包。参数包的整体使用前面要加上“...”说明*/
}
int main()
{
func(); //无参形式
func(1, 1, 2); //同类型参数形式
func(1, 1.2, "bit", 'a'); //不同类型参数形式
return 0;
}
上面的参数Args前面有省略号,表示它是一个参数表,函数形参args同理。它们里面包含了0到N(N>=0)个模版参数,当函数实例化后,以上面func(1, 1.2, "bit", 'a');为例,编译器将会在编译时生成void func<int,double,const char*,char>(其它形式同理)。
由于参数个数和类型不定,所以使用sizeof输出的是参数的个数。下面的问题是又该如何使用包里面的参数?我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。参数包中的个数可以使用sizeof获取,有些人可能会使用数组的形式使用,但C++11语法并不支持使用args[i]这样的方式获取可变参数。
递归函数方式展开参数包
C++11使用递归函数方式展开参数包,具体形式类似于以下代码:
#include <iostream>
using namespace std;
//编译时递归返回条件,即递归终止函数
void _func() { cout << endl; }
//注意: 递归返回条件不能使用下面的,因为出现无参函数时将会出错,即出现func()的情况
//template <class T>
//void _func(const T& data) { cout << data << endl; }/*编译时递归解析,依次拿到参数包args里面的参数传递给T类型的data,直到参数包args为空时调用递归终止函数_func()*/
template <class T, class... Args>
void _func(const T& data, Args ...args)
{
cout << data << " ";
_func(args...); //递归遍历
}//参数包args中有0-N个参数
template <class ...Args>
void func(Args ...args)
{
_func(args...); //传入参数包
}
int main()
{
func();
func(1);
func(1, 1, 1);
func(1, 1.2, "bit", 'a');
return 0;
}
模板在编译阶段实例化,编译器实例化后将会把参数包展开,这里的递归过程可理解为参数的诼渐展开过程,以func(1, 1.2, 'a');为例,展开过程如下:
//编译阶段中,func(1, 1.2, 'a')在主函数传入模板被实例化后,将会推演生成以下类似的代码
void func(int val1, double ch, char s)
{
_func(val1, ch, s);
}//诼渐往下抽象递归解包参数,直到遍历到递归终止函数
void _func(const int& data, double ch, char s)
{
cout << data << " ";
_func(ch, s); //往下调用void _func(const double& data, char s)
}void _func(const double& data, char s)
{
cout << data << " ";
_func(s); //往下调用void _func(const char& data)
}void _func(const char& data)
{
cout << data << " ";
_func(); //递归终止函数
}
逗号表达式展开参数包
逗号表达式的思想是通过展开表达式来依次拿到参数包里面的参数,与递归不同,系统调用时会生成N个表达式,然后通过这些表达式分别调用对应参数的模板函数,如下:
//模板函数
template <class T>
void _Func(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void Func(Args... args)
{
int arr[] = { (_Func(args), 0)... }; /*使用逗号表达式(PrintArg(args), 0)...展开参数包args中参数,直到参数包为空*/cout << endl;
}
int main()
{
Func(1);
Func(1, 'A');
Func(1, 'A', std::string("sort"));
return 0;
}
分析:
运用时,{(_Func(args), 0)...}将会展开成{(_Func(arg1),0), (_Func(arg2), 0), (Func(arg3), 0), ... )},以上面Func(1, 'A')为例,这里会创建{(_Func(1),0), (_Func('A'), 0)},最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分_Func(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。虽然这个数组无用,但是若直接运用{(_Func(args), 0)...}表达式将不会自动展开,因为它没有类似数组的initializer_list展开,直接运用时系统报错。
明白了上面原理后可将上面逗号表达式修改为以下式子,至于原因可根据上面的分析思考,若明白了这一原理后说明这方面已经理解。
//模板函数
template <class T>
int PrintArg(T t)
{
cout << t << " ";
return 0;
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { PrintArg(args)... };
cout << endl;
}
参数包的综合运用
参数包“...”(三个点)的运用情况还有很多。“...”的位置不同,它所对应的情况也有所不同。这方面的逻辑调用分很多情况,但具体的运用理解思路对应的也就以下几种情况。
//样例一:
template <class ...Args> /*“...”放在参数包Args的左边,表示中间有N个参数打包成了Args,运用时将类型参数全部展开*/
void ShowList(Args... args) /*“...”表示参数包中有N个类型,为每个类型在Args中生成一个对应的函数参数,将其打包成args,运用时将形参和类型全部展开*/运用时为了方便记忆,上面参数包“...”的定义都放在函数参数包和模板参数包的左边
//样例二
void _func() { cout << endl; }
template <class T, class... Args>
void _func(const T& data, Args ...args)
{
_func(args...); /*“...”放在“()”里面args的右边,表示具有N个类型,调用时只将传入的形参全部展开,如上面_func(val1, ch, s)递归展开*/
}
//样例三:template <typename ...Args>
struct A
{
T _data;
template<class ...Args>
A(Args&&... args): _data(forward<Args>(args)...) {} /*构造函数模板参数包的万能引用。“...”在参数包args括号“()”外面的右边,表示具有N个forward<Args>(args)式子,调用时会将其表达式展开,如同上面的逗号表达式*/
};参数包的调用时,“...”要放在其后面,表示存在N个参数或式子,当“...”直接放在参数包名称的后面时,只是表示参数具有N个,调用时会将参数全部展开,如上面_func(val1, ch, s);当“...”放在表达式的后面时,表示表达式具有N个,调用时会将表达式全部展开,如上面逗号表达式的式子
模板参数包灵活运用的基本理解如上,其它语法运用基本都是固定语法,如sizeof...(args)。若是出现这一固定搭配可自行查看专门文档,这里不再一一说明。
明白了这一原理后这里会发现一个潜在的问题,当传入的参数数量过多,即参数包过大,递归层数过多,系统会支撑不住这么大的开销,发生错误,实际应用中应注意。类似于以下代码:
template<size_t N>
void fun()
{
cout << N << " ";
fun<N - 1>();
}template<>
void fun<1>()
{
cout << 1 << endl;
}int main()
{
fun<15000>(); //传入参数数量过大,递归层数过多,系统支撑不下
return 0;
}
2-2,STL容器empalce的相关接口
C++中的emplace
是一个常用的技术,主要用于在容器中就地构造元素,而不是先创建一个元素,然后将其复制到容器中。这种方法通常比先创建再复制(或移动)更高效,因为它减少了不必要的临时对象的创建和销毁。
emplace
通常在标准模板库(STL)的容器中看到,如vector
, list
, map
, set
等。每个这些容器都可能有一个或多个名为emplace_back
, emplace_front
, emplace
等的成员函数。下面我们以list
容器中的emplace_back
和push_back
为例展示。
其中emplace_back
底层的接口是万能引用参数包,push_back
底层的接口是const左值引用和右值引用。
示例:
假设我们有一个vector<string>
,并且我们想要添加一个新的字符串。使用传统的push_back
插入方法类似于以下:
vector<string> v;
//使用v.push_back("bit");内部形式如同以下
string str = "bit"; //先构造出string的临时对象
v.push_back(str); //然后将此对象移动拷贝到v容器中
若使用emplace
我们可以直接在v
中构造字符串,从而避免了不必要的复制(或移动)。
vector<string> vec;
vec.emplace_back("bit"); //直接在容器中构造新的string对象,没有创建临时的string对象
这里不难发现,若是直接传递对象,emplace
将不会做出任何改变。emplace
的主要作用是为了减少不必要的拷贝,如同string str("bit")的优化,将拷贝+拷贝构造直接优化成拷贝构造,若是没有不必要的构造,emplace
与其它功能相同的函数的使用没有任何区别。还有就是emplace
不能使用"{}"(初始化列表)。主要是因为emplace函数不是通过列表初始化来构造对象的,而是使用直接初始化或拷贝初始化,因为多参数要进行隐式类型转换,若进行隐式类型转换就必须要构造出临时对象,与底层底层结构不匹配。当出现多参数时这里可直接使用“()”。
下面我们使用上面模拟实现的string类来观察这一现象。
#include <list>
#include "String.h"
int main()
{
list<bit::string> lt1;
bit::string s1("xxxx");
lt1.push_back(s1); //调用拷贝构造bit::string在容器中创建string对象
cout << endl;
lt1.push_back(move(s1)); //调用移动拷贝bit::string将资源转移到容器string对象中
cout << "=============================================" << endl;
//对象传递,没有临时对象的不必要拷贝,emplace_back的调用输出跟push_back一样
bit::string s2("xxxx");
lt1.emplace_back(s2);
cout << endl;
lt1.emplace_back(move(s2));
cout << "=============================================" << endl;
//先将"xxxx"构造成bit::string对象,然后拷贝到lt1容器中
lt1.push_back("xxxx");
cout << endl;
//直接在lt1容器中构造数据是"xxxx"的bit::string对象
lt1.emplace_back("xxxx");
cout << "=============================================" << endl;
list<pair<bit::string, bit::string>> lt2;
//下面输出两个重复的信息是因为pair是两个指令
pair<bit::string, bit::string> kv1("xxxx", "yyyy");
lt2.push_back(kv1);
lt2.push_back(move(kv1));
cout << "=============================================" << endl;pair<bit::string, bit::string> kv2("xxxx", "yyyy");
//对象的传递,即便存储类型是pair键值对也是同理,与push_back输出一样
lt2.emplace_back(kv2);
lt2.emplace_back(move(kv2));
cout << "=============================================" << endl;
lt2.emplace_back("xxxx", "yyyy");
cout << endl;
lt2.push_back({ "xxxx", "yyyy" });
cout << "=============================================" << endl;
return 0;
}
通过以上可发现,由于emplace
只接受非对象时的改进,而非对象在这方面的运用是构造+移动语义,emplace
的改进只是省去了移动语义。移动语义只是移动资源,内部消耗很小,也就是说emplace
的改善在实现有移动语义类的情况下效率没有多大提高,但是在没有实现有移动语义类的情况下效率将大大提升,如push_back内部是构造+拷贝构造,emplace_back内部是构造。
class Date
{
public:
Date(int year, int month, int day)
:_year(year), _month(month), _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
Date(const Date& d) //拷贝构造,没有移动语义
:_year(d._year), _month(d._month), _day(d._day)
{
cout << "Date(const Date& d)" << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};int main()
{
list<Date> lt1;
lt1.push_back({ 2024,3,30 });
cout << endl;
lt1.emplace_back(2024, 3, 30);
cout << endl << endl;//匿名对象。匿名对象也是对象,emplace与push的实现相同
lt1.push_back(Date(2023, 1, 1));
cout << endl;
lt1.emplace_back(Date(2023, 1, 1));
return 0;
}