目录
一、新的类功能
-
默认的移动构造函数和移动赋值函数:
使用一个类的右值对象去初始化同类对象和为同类对象赋值时,在该类自定义了移动构造函数和移动赋值函数的情况下,会调用该类自定义的移动构造函数和移动赋值函数。
而在没有自定义移动构造函数和移动赋值函数的情况下,如果没有自定义拷贝构造函数、赋值运算符重载以及析构函数,那么编译器会自动生成默认的移动构造函数和移动赋值函数。
默认的移动构造函数和移动赋值函数,对于内置类型的成员变量,会执行逐成员按字节拷贝;对于自定义类型的成员变量,如果该成员自定义了移动构造函数和移动赋值函数,那么调用对应的移动构造函数和移动赋值函数,否则调用拷贝构造函数和赋值运算符重载。
#include "string.h" class Person { public: Person(const char* name = "", int age = 0) : _name(name), _age(age) { } private: yzz::string _name; int _age; }; int main() { Person p1("张三", 18); Person p2 = std::move(p1); // string(string&& rr) Person p3("李四", 19); Person p4; p4 = std::move(p3); // string& operator=(string&& rr) return 0; }
-
强制生成默认函数的关键字 default:
class Person { public: Person(const char* name = "", int age = 0) : _name(name), _age(age) { } Person(const Person& p) : _name(p._name), _age(p._age) { } // 自定义了拷贝构造函数后,编译器就不会生成默认的移动构造函数和移动赋值函数, // 此时就需要使用 default 关键字显示指定默认的移动构造函数和移动赋值函数的生成 Person(Person&& p) = default; Person& operator=(Person&& p) = default; private: yzz::string _name; int _age; }; int main() { Person p1("张三", 18); Person p2 = std::move(p1); // string(string&& rr) Person p3("李四", 19); Person p4; p4 = std::move(p3); // string& operator=(string&& rr) return 0; }
二、在模板中使用可变参数
2.1 - 可变参数函数
C/C++ 一直都支持为函数设置可变参数,最典型的代表就是 printf() 函数,它的语法格式为:
int printf(const char* format, ...);
其中 ...
表示的是可变参数(参数包),即 printf() 函数可以接受任意个参数,且各个参数的类型可以不同,例如:
printf("\n");
printf("%d\n", 10);
printf("%d %c\n", 10, 'a');
printf("%d %c %lf\n", 10, 'a', 3.14);
下面的程序中,自定义了一个简单的可变参数函数:
#include <cstdarg>
#include <iostream>
using namespace std;
void func(int count, ...)
{
va_list args_ptr;
va_start(args_ptr, count);
for (int i = 0; i < count; ++i)
{
int arg = va_arg(args_ptr, int);
cout << arg << endl;
}
va_end(args_ptr);
}
int main()
{
// 可变参数有 4 个,分别为 10、20、30、40
func(4, 10, 20, 30, 40);
return 0;
}
func() 函数中有 2 个参数,一个是 count,另一个就是
...
可变参数,我们很容易在函数内部使用 count 参数,但想要使用参数包中的参数,需要借助 <cstdarg> 头文件中的 va_list 类型以及 va_start、va_arg、va_end 这三个带参数的宏。其中 va_list 可以简单地视为 char*,即:
typedef char* va_list;
而要理解 va_start、va_arg、va_end 是如何实现的,需要先理解参数是如何传递给函数的。
函数的数据是放在栈中的,给一个函数传递参数的过程就是将函数的参数从右往左逐次压栈。例如:给
func(int i, char c, double d)
函数传递参数的过程就是将 d、c、i 逐次压到函数的栈中,并且由于栈是从高地址向低地址扩展的,所以 d 的地址最高,i 的地址最低。理解了给一个函数传递参数的过程,就可以理解 va_start、va_arg、va_end 是如何实现的。
// 让 ap 指向第一个可变参数 #define my_va_start(ap, x) ( ap = (my_va_list)(&x + 1) ) // 让 ap 指向下一个可变参数,并将 ap 指向的上一个可变参数提取出来 #define my_va_arg(ap, t) ( *(t*)((ap += sizeof(t)) - sizeof(t)) ) // 销毁 ap #define my_va_end(ap) ( ap = (my_va_list)0 )
2.2 - 可变参数模板
C++11 之前,函数模板和类模板只能设定固定数量的模板参数;C++11 对模板的功能进行了扩展,允许模板中包含任意数量的模板参数,这样的模板又称为可变参数模板。
2.2.1 - 可变参数函数模板
如下定义了一个可变参数的函数模板:
template<class... Types>
void ShowList(Types... args)
{
// 函数体
}
在模板参数中,class 或者 typname 后面跟 ...
就表明 Types 是一个可变模板参数,它可以接受多种数据类型,又称模板参数包。
在函数参数中,args 的参数类型用 Types...
表示,表明 args 可以接受任意个参数,又称函数参数包。
使用可变参数模板的难点在于,如何在模板函数内部"解开"参数包(使用包内数据)。
-
【递归方式解包】:
#include <iostream> using namespace std; // 递归结束的出口 void ShowList() { cout << endl; } template<class T, class... Types> void ShowList(T val, Types... args) { cout << val << " "; ShowList(args...); } int main() { ShowList(); ShowList(10); ShowList(10, 'a'); ShowList(10, 'a', 3.14); ShowList(10, 'a', 3.14, "hello"); return 0; }
注意:以递归方式解包,一定要设置递归结束的出口。在上例中,无形参的 ShowList() 函数就是递归结束的出口。
-
【借助列表初始化和逗号表达式】:
#include <iostream> using namespace std; template<class T> void display(T val) { cout << val << " "; } template<class...Types> void ShowList(Types...args) { cout << sizeof...(args) << " : "; int arr[] = { (display(args), 0)... }; cout << endl; } int main() { ShowList(10, 'a', 3.14); return 0; }
我们使用了
{ }
列表初始化的方式对数组 arr 进行了初始化,(display(args), 0)...
会依次展开为(display(10), 0)
、(display(‘a'), 0)
和(display(3.14), 0)
。每个元素都是一个逗号表达式,以
(display(10), 0)
为例,会先计算 display(10),然后再将 0 作为整个表达式的值返回给数组,因此 arr 数组中最终存储的都是 0,arr 数组纯粹是为了将参数包展开,没有发挥其他作用。
STL 容器中的 emplace 相关接口:
list::emplace_back - C++ Reference (cplusplus.com)。
vector::emplace_back - C++ Reference (cplusplus.com)。
template<class... Args>
void emplace_back(Args&&... args);
emplace_back 相较于 push_back,优势在哪呢?
#include <list>
#include "string.h"
#include <iostream>
using namespace std;
int main()
{
list< pair<int, char> > lt1;
lt1.push_back({ 10, 'a' });
lt1.push_back(make_pair(20, 'b'));
// emplace 支持可变参数,拿到构造 pair 对象的参数后可以自己去创建对象,
// 这里只有用法上的区别
lt1.emplace_back(30, 'c');
lt1.emplace_back(make_pair(40, 'b'));
list< pair<int, yzz::string> >lt2;
lt2.push_back({ 10, "hello" });
lt2.push_back(make_pair(20, "world"));
// string(string&& rr)
// string(string && rr)
cout << "-------------------------" << endl;
// push_back 是先构造,再移动构造;而 push_back 是直接构造了
lt2.emplace_back(30, "你好");
lt2.emplace_back(make_pair(40, "世界"));
return 0;
}
2.2.2 - 可变参数类模板
C++11 中,类模板中的模板参数也可以是一个可变参数。C++11 提供的 tuple 元组类就是一个典型的可变参数类模板,它的定义如下:
template<class... Types> class tuple;
如下代码展示了一个支持可变参数的类模板:
#include <iostream>
using namespace std;
// 声明可变参数类模板
template<class... Values> class demo;
// 继承式递归的出口
template<> class demo<> {};
// 以 "继承 + 递归" 的方式解包
template<class Head, class... Tail>
class demo<Head, Tail...> : private demo<Tail...>
{
public:
demo(Head v, Tail... vtail)
: _head(v), demo<Tail...>(vtail...)
{
display_head();
}
void display_head() const
{
cout << _head << endl;
}
protected:
Head _head;
};
int main()
{
demo<int, char, double> d(10, 'a', 3.14);
// 3.14
// a
// 10
return 0;
}
程序中,继承关系可以表述为:
demo<>
↓
demo<double>
↓
demo<char, double>
↓
demo<int, char, double>
可变参数类模板还有其他的解包方法,这里不再一一赘述。