🌈欢迎来到C++专栏~可变参数模板
- (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是Scort
- 目前状态:大三非科班啃C++中
- 🌍博客主页:张小姐的猫~江湖背景
- 快上车🚘,握好方向盘跟我有一起打天下嘞!
- 送给自己的一句鸡汤🤔:
- 🔥真正的大师永远怀着一颗学徒的心
- 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏
- 🎉🎉欢迎持续关注!
文章目录
- 🌈欢迎来到C++专栏~可变参数模板
- 一. 类的新功能
- 🥑默认成员函数
- 🥑类成员变量初始化
- 🥑强制生成默认函数的关键字`default`
- 🥑禁止生成默认函数的关键字delete
- 二. 可变模板参数
- 💦模板定义
- 💦参数包的展开
- 😎递归函数方式展开
- 😎逗号表达式展开
- 三. emplace
- ✨使用方法
- ✨工作原理
- ✨意义
- 📢写在最后
一. 类的新功能
🥑默认成员函数
C++11之后,有八个默认成员函数
在C++11之前,一个类中有如下六个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值函数
- 取地址重载函数
- const取地址重载函数
其中前四个成员函数最重要,后面两个成员函数一般不会用到,这里“默认”的意思就是你不写编译器会自动生成。在C++11标准中又增加了两个默认成员函数,分别是移动构造函数和移动赋值重载函数
✨默认移动构造和移动赋值的生成条件
- 默认移动构造生成条件:没有自己实现移动构造函数,并且没有自己实现析构函数、拷贝构造函数和拷贝赋值函数的任意一个
- 移动赋值重载函数的生成条件:没有自己实现移动赋值重载函数,并且没有自己实现析构函数、拷贝构造函数和拷贝赋值函数
😎特别注意: 如果我们自己实现了移动构造或者移动赋值,就算没有实现拷贝构造和拷贝赋值,编译器也不会生成默认的拷贝构造和拷贝赋值
默认生成的移动构造和移动赋值会做什么?
- 对于内置类型的成员会完成值拷贝(浅拷贝),对于自定义类型的成员,如果该成员实现了移动构造/移动赋值就调用它的移动构造/移动赋值,否则就调用它的拷贝构造
对此我们展开验证,这里需要模拟实现一个简化版的string类,类当中只编写了几个我们需要用到的成员函数。
代码如下:
namespace ljj
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str; //返回字符串中第一个字符的地址
}
iterator end()
{
return _str + _size; //返回字符串中最后一个字符的后一个字符的地址
}
//构造函数
string(const char* str = "")
{
_size = strlen(str); //初始时,字符串大小设置为字符串长度
_capacity = _size; //初始时,字符串容量设置为字符串长度
_str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0')
strcpy(_str, str); //将C字符串拷贝到已开好的空间
}
//交换两个对象的数据
void swap(string& s)
{
//调用库里的swap
::swap(_str, s._str); //交换两个对象的C字符串
::swap(_size, s._size); //交换两个对象的大小
::swap(_capacity, s._capacity); //交换两个对象的容量
}
//拷贝构造函数(现代写法)
string(const string& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象
swap(tmp); //交换这两个对象
}
//移动构造
string(string&& s)//右值引用
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 资源转移" << endl;
swap(s); //资源互换
}
//赋值运算符重载(现代写法)
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 深拷贝" << endl;
string tmp(s); //用s拷贝构造出对象tmp
swap(tmp); //交换这两个对象
return *this; //返回左值(支持连续赋值)
}
//移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(const string&& s) -- 移动赋值" << endl;
swap(s);
return *this; //返回左值(支持连续赋值)
}
//析构函数
~string()
{
delete[] _str; //释放_str指向的空间
_str = nullptr; //及时置空,防止非法访问
_size = 0; //大小置0
_capacity = 0; //容量置0
}
//[]运算符重载
char& operator[](size_t i)
{
assert(i < _size); //检测下标的合法性
return _str[i]; //返回对应字符
}
//改变容量,大小不变
void reserve(size_t n)
{
if (n > _capacity) //当n大于对象当前容量时才需执行操作
{
char* tmp = new char[n + 1]; //多开一个空间用于存放'\0'
strncpy(tmp, _str, _size + 1); //将对象原本的C字符串拷贝过来(包括'\0')
delete[] _str; //释放对象原本的空间
_str = tmp; //将新开辟的空间交给_str
_capacity = n; //容量跟着改变
}
}
//尾插字符
void push_back(char ch)
{
if (_size == _capacity) //判断是否需要增容
{
reserve(_capacity == 0 ? 4 : _capacity * 2); //将容量扩大为原来的两倍
}
_str[_size] = ch; //将字符尾插到字符串
_str[_size + 1] = '\0'; //字符串后面放上'\0'
_size++; //字符串的大小加一
}
//+=运算符重载
string& operator+=(char ch)
{
push_back(ch); //尾插字符串
return *this; //返回左值(支持连续+=)
}
//返回C类型的字符串
const char* c_str()const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
再编写一个简单的Person类,Person类中的成员name的类型就是我们模拟实现的string类
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:
cl::string _name; //姓名
int _age; //年龄
};
因为Person类中实现了拷贝构造、拷贝赋值和析构函数等,所以不会默认生成移动构造和移动赋值
int main()
{
Person s1("张三", 7);
Person s2 = s1;//拷贝构造
Person s3 = std::move(s1);//移动构造(没有移动构造,就会调用拷贝构造)
return 0;
}
ps:由于VS2013没有完全支持C++11,因此上述代码无法在VS2013当中验证,需要使用更新一点的编译器进行验证,比如VS2019
🥑类成员变量初始化
默认生成的构造函数,对于自定义类型的成员会调用其构造函数进行初始化,但并不会对内置类型的成员进行处理。于是C++11支持非静态成员变量在声明时进行初始化赋值,默认生成的构造函数会使用这些缺省值对成员进行初始化
class Person
{
public:
//...
private:
//非静态成员变量,可以在成员声明时给缺省值
ljj::string _name = "张三"; //姓名
int _age = 20; //年龄
static int _n; //静态成员变量不能给缺省值
};
🥑强制生成默认函数的关键字default
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default
关键字显示指定移动构造生成
下面函数实现了拷贝构造
class Person
{
public:
//拷贝构造函数
Person(const Person& p)
:_name(p._name)
, _age(p._age)
{}
private:
cl::string _name; //姓名
int _age; //年龄
};
这样编译器就无法生成默认的构造函数了,因为默认构造函数生成的条件是没有编写任意类型的构造函数,包括拷贝构造函数
这时我们就可以使用default
关键字强制生成默认的构造函数
class Person
{
public:
Person() = default; //强制生成默认构造函数
//拷贝构造函数
Person(const Person& p)
:_name(p._name)
, _age(p._age)
{}
private:
cl::string _name; //姓名
int _age; //年龄
};
主要使用场景:用于构造函数,因为如果我们实现了拷贝构造(也算构造),就不会默认生成构造函数了,需要强制生成
🥑禁止生成默认函数的关键字delete
当我们想要限制某些默认函数生成
- 在
C++98
中,可以将该函数设置成私有,并且只用声明不用定义,这样当外部调用该函数时就会报错。 - 在
C++11
中,可以在该函数声明后面加上=delete
,表示让编译器不生成该函数的默认版本,我们将=delete修饰的函数称为删除函数
class Person
{
public:
//构造函数
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
//不想要Person对象拷贝
Person(const Person& p) = delete;
private:
ljj::string _name; //姓名
int _age; //年龄
};
int main()
{
Person s1("张三", 7);
Person s2 = s1;//拷贝构造
return 0;
}
来道题目:要求delete关键字实现,一个类只能在堆上创建对象
class HeapOnly
{
public:
HeapOnly()
{
_str = new char[10];
}
~HeapOnly() = delete;
void Destroy()
{
delete[] _str;//删除开创的空间
free(this);//删除ptr指针
}
private:
char* _str;
};
int main()
{
HeapOnly* ptr = new HeapOnly;
ptr->Destroy();
return 0;
}
如果是构造时,对象有数据,则要我们手动实现一个类似析构函数的函数,析构要注意两块空间哦:ptr指针和new的空间都要释放了
二. 可变模板参数
C++11
新增一员大将就是可变参数模板,他可以允许可变参数的函数模板和类模板来作为参数,使得参数高度泛化
💦模板定义
函数的可变参数模板定义方式如下:
template<class …Args>
void ShowList(Args… args)
{
//函数体
}
- Args是一个模板参数包,args是一个函数形参参数包
声明一个参数包Args…args,这个参数包中可以包含0到任意个模板参数
模板参数包 Args 和函数形参参数包 args 的名字可以任意指定,并不是说必须叫做 Args 和 args
int main()
{
//函数传参就可以传多个不同类型参数了
string str("hello");
ShowList();
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', str);
return 0;
}
也可以通过sizeof算出参数包中参数的个数:...
是在外面的
template<class ...Args>
void ShowList(Args... args)
{
cout << sizeof...(args) << endl; //获取参数包中参数的个数
}
此时最大的难点来了,就是怎么样直接获取参数包中的每个参数?
- 语法并不支持使用
args[i]
的方式来获取参数包中的参数,只能通过展开参数包的方式来获取,这是使用可变参数模板的一个主要特点
下面是错误示范:(语法不支持!)
template<class ...Args>
void ShowList(Args... args)
{
cout << sizeof...(args) << endl;
//错误示例:
for (int i = 0; i < sizeof...(args); i++)
{
cout << args[i] << " "; //打印参数包中的每个参数
}
cout << endl;
}
💦参数包的展开
😎递归函数方式展开
该方法大概分为三步:
- 给函数模板增加一个模板参数,从接收的参数包中把第一个参数分离出来
- 在函数模板中递归调用该函数模板,调用时传入剩下的参数包
- 直到递归到参数包为空,退出递归
//可变参数的函数模板
template<class T, class ...Args>
void ShowList(const T& val, Args... args)
{
cout << val << " ";//打印分离出的第一个参数
ShowList(args...);//继续递归调用
}
//递归至空,终止函数
void ShowList()
{
cout << endl;
}
int main()
{
string str("hello");
ShowList(1, 'A', str);
return 0;
}
那如果我们不使用递归调用,该怎么样写呢?
😎逗号表达式展开
逗号表达式规则是会从左到右依次计算各个表达式,并将最后一个表达式的值作为返回值返回,我们将最后一个表达式设为整型值,确保最后返回的是一个整型
将处理参数个数的动作封装成一个函数,将该函数作为逗号表达式的第一个表达式
template <class T>
void PrintArg(const T& x)
{
cout << x << " ";
}
template <class ...Args>
void ShowList(Args... args)
{
int a[] = { (PrintArg(args), 0)... };//开创输入参数个整数类型的数组
}
我们要的是打印出参数包中的各个参数,因此处理函数当中要做的就是将传入的参数进行打印即可
ps:可变参数的省略号
...
需要加在逗号表达式外面,表示需要先将逗号表达式展开,如果直接加在 args 后面,那么参数包将会被展开后全部传入 PrintArg,会展开成 {(PrintArg(arg1), 0), (PrintArg(arg2), 0), (PrintArg(arg3), 0), etc…}
当然我们不使用逗号表达式也是可以的,这里的问题是初始化整型数组时必须用整数,那我们可以将处理函数的返回值设为整型,然后用这个返回值去初始化整型数组也是可以的:
void ShowList()
{
cout << endl;
}
//处理函数
template<class T>
int PrintArg(const T& t)//返回值为int类型
{
cout << t << " ";
return 0;
}
//展开函数
template<class ...Args>
void ShowList(Args... args)
{
int arr[] = { PrintArg(args)... }; //列表初始化
cout << endl;
}
三. emplace
C++11 给 STL 容器增加 emplace
的插入接口,比如 list 容器的 push_front、push_back 和insert 函数,都有了对应的 emplace_front、emplace_back 和 emplace 函数:
这些emplace
版本的插入接口支持模板的可变参数,比如vector容器的emplac函数的声明如下:
✨使用方法
调用 push_back 插入元素时,可以传入左值对象或右值对象,也可以使用列表进行初始化;调用emplace_back 插入元素时,也可以传入左值对象或右值对象,但不可以使用列表进行初始化
emplace
系列接口最大的特点就是,插入元素可传入用于构造元素的参数包
int main()
{
//对于整形:没有区别
vector<int> v;
v.push_back(1);
v.emplace_back(2);
//对于pair类型数据
vector<pair<std::string, int>> v1;
v1.push_back(make_pair("sort", 1));
v1.emplace_back("sort", 1);
return 0;
}
那么emplace会比push_back更加高效吗?不一定!
✨工作原理
emplace
接口先通过空间配置器为新结点获取一块内存空间,注意这里只会开辟空间,不会自动调用构造函数对这块空间进行初始化
- 调用 allocator_traits::construct 函数对这块空间进行初始化,调用该函数会传入这块空间的地址和用户传入的参数,注意要完美转发
- 在 allocator_traits::construct 中会使用定位 new 表达式,显示调用构造函数对这块空间进行初始化,调用构造函数时会传入用户传入的参数,这里同样需要完美转发
✨意义
emplace 接口的可变参数模板类型都是万能引用,因此既可以接收左值,也可以接收右值,还可以接收参数包
- 调用
emplace
接口时传入的是参数包,就可以直接调用函数进行插入,并最终使用定位 new 表达式调用构造函数对空间进行初始化时,匹配到构造函数 - 调用 emplace 接口时传入的是左值or右值,首先需要先在此之前调用构造函数实例化出一个对象,最后使用定位 new 表达式调用构造函数对空间进行初始化时,会匹配到拷贝构造函数or拷贝构造
总的来说:如果传入参数包,只需要调用构造函数
emplace 最大特点就是支持传入参数包,用这些参数包直接构造出对象,这样就能减少一次拷贝,这就是为什么有人说 emplace 系列接口更高效的原因
emplace 真正高效的情况是传入参数包的时候, 直接通过参数包构造出对象,避免了中途的一次拷贝 直接通过参数包构造出对象,避免了中途的一次拷贝
举例演示:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{
cout << "构造函数" << endl;
}
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "拷贝构造" << endl;
}
Date& operator=(const Date& d)
{
cout << "赋值重载" << endl;
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
list<Date> lt1;
lt1.push_back(Date(2023, 2, 3));
cout << endl;
lt1.emplace_back(2023, 2, 3);
}
所以验证了emplace_back是比push_back要少一次拷贝构造