1. 可变参数模板
C++11的新特性可变参数模板能够让大家创建可以接受可变参数的函数模板和类模板,相比 C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段我们掌握一些基础的可变参数模板特性就够我们用了,所以这里点到为止,以后如果有需要,再可以深入学习。
下面就是一个基本可变参数的函数模板
template <class ...Args> void ShowList(Args... args) { //....... }
- 声明一个参数包Args...args,这个参数包中可以包含0到N(N>=0)个模板参数。
- Args:是一个模板参数包
- args:是一个函数形参参数包
上面的参数args前面有省略号,所以它就是一个可变模版参数,带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数, 只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以要用一些奇招来壹壹获取参数包的值。
在使用可变参数模板的时候,可以传入任意个类型的数据,编译器会将所有类型打包。可变参数模板的难点就是如果展开参数包,从而使用里面的每个模板参数。
1.1 展开参数包
1.1.1 递归函数方式展开
template <class T>// 递归终止函数 void ShowList(const T& t) { cout << t << endl; } template <class T, class ...Args>// 展开函数 void ShowList(T value, Args... args) { cout << value << " "; ShowList(args...); } int main() { ShowList(1); ShowList(1, 'R'); ShowList(1, 'R', std::string("left")); return 0; }
调用同一个函数模板,传入不同个数的参数,函数模板都能将这些变化的参数打印出来。
如上图灵魂画手画的图,这种方式很像递归,在函数模板中调用函数模板,通过模板参数中的第一个模板参数一个个从参数包中拿参数。不需要的形参的函数就相当于一个结束条件。
1.1.2 逗号表达式展开
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, 'R'); ShowList(1, 'R', std::string("left")); return 0; }
同样将参数包挨个展开了。
- 逗号表达式的结果是最右边的值。
这种展开参数包的方式,不需要通过递归终止函数,是直接在ShowList函数体中展开的, PrintArg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。
我们知道逗号表达式会按顺序执行逗号前面的表达式。 ShowList函数中的逗号表达式:(PrintArg(args), 0),也是按照这个执行顺序,先执行 PrintArg(args),再得到逗号表达式的结果0。
同时还用到了C++11的另外一个特性:初始化列表,通过初始化列表来初始化一个变长数组, {(PrintArg(args), 0)...}将会展开成((PrintArg(arg1),0), (PrintArg(arg2),0), (PrintArg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分PrintArg(args) 打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
逗号表达式简化的代码:
template <class T> int PrintArg(T t) { cout << t << " "; return 0; } template <class ...Args>//展开函数 void ShowList(Args... args) { int arr[] = { PrintArg(args)... }; cout << endl; } int main() { ShowList(1); ShowList(1, 'R'); ShowList(1, 'R', std::string("left")); return 0; }
采用上图所示方式也可以展开参数包。
- 在ShowList中的数组中多次调用PrintArg函数,每次调用后返回值是0。
- 多个0形参的列表初始化数组。
这种方式中,看起来比逗号表达式好理解,数组同样仅起辅助作用。
1.2 emplace相关接口
C++11基于可变参数模板在STL中提供了emplace相关的接口:
上图以vector为例,其他STL容器也有emplace系列的相关接口。emplace的作用和insert类似,emplace_back的作用和push_back相似。
emplace接口也是模板函数,它既是一个万能引用模板也是一个可变参数模板,可以称为万能引用可变参数模板。无论插入的数据是左值还是右值,无论是多少个,都可插入。
int main() { list<int> mylist; mylist.push_back(10); mylist.push_back(20); mylist.emplace_back(30); mylist.emplace_back(40); //mylist.emplace_back(50, 60); //不能这样用 for (const auto& e : mylist) { cout << e << endl; } return 0; }
对于内置类型,push_back和emplace_back没有任何区别。而且也不可以一次性插入多个内置类型的值。只有对容器实例化后,并且存放多个值时,才能使用empalce_back一次性插入:
int main() { list<pair<int, char>> mylist; mylist.push_back({ 10, 'a' }); mylist.emplace_back(20, 'b'); mylist.push_back(make_pair(30, 'c')); mylist.emplace_back(make_pair(40, 'd')); mylist.emplace_back(50, 'e'); for (const auto& e : mylist) { cout << e.first << ":" << e.second << endl; } return 0; }
emplace相关接口的优势:
将上篇文章https://blog.csdn.net/GRrtx/article/details/131609269中的string复制过来,在用字符串的构造的构造函数中打印提示信息,使用push_back插入不同类型的值:
namespace rtx { class string { public: string(const char* str = "") :_size(strlen(str)) , _capacity(_size) { _str = new char[_capacity + 1]; strcpy(_str, str); cout << "string(const char* str = "") -- 构造函数" << endl; } void swap(string& s) { ::swap(_str, s._str); ::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); 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=(string s) -- 拷贝赋值(深拷贝)" << endl; string tmp(s); swap(tmp); return *this; } string& operator=(string&& s) // 移动赋值 { cout << "string& operator=(string s) -- 移动赋值(资源移动)" << endl; swap(s); return *this; } ~string() { delete[] _str; _str = nullptr; } void reserve(size_t n) { if (n > _capacity) { char* tmp = new char[n + 1]; strcpy(tmp, _str); delete[] _str; _str = tmp; _capacity = n; } } void push_back(char ch) { if (_size >= _capacity) { size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2; reserve(newcapacity); } _str[_size] = ch; ++_size; _str[_size] = '\0'; } protected: char* _str; size_t _size; size_t _capacity; }; } int main() { std::list< std::pair<int, rtx::string> > mylist; mylist.emplace_back(10, "sort"); mylist.emplace_back(make_pair(20, "sort")); cout << "#########################################" << endl; mylist.push_back(make_pair(30, "sort")); mylist.push_back({ 40, "sort" }); return 0; }
据此对比发现:
插入左值时,emplace_back和push_back没有区别。
因为左值无论是编译器还是emplace_back都是不敢进行优化的,只能老老实实进行深拷贝,以防影响到原本的左值。
插入右值(匿名键值对)时,emplace_back仅调用了构造函数。
在插入的过程中,匿名对象一直存在,没有被转移资源,知道链表在new一个新节点的时候,才用右值对象中的数据来初始化节点,其中string调用的是普通构造函数,是用右值中的字符串来初始化的。
插入多个值(可变参数)时,emplace_back仅调用了构造函数。
和插入右值一样,只有在new一个新节点的时候,多个插入的值才被用来初始化,所以也是只调用了普通构造函数。
只有在插入自定义类型的右值时,emplace_back的效率才比push_back高。
emplace_back比push_back少调用了一个移动构造函数
我们知道,移动构造是将右值的资源进行转移,也是非常高效的,代价非常小。
emplace系列接口在存在移动构造的情况下,并不能比push_back高效很多,但还是高一点的。
如果没有移动构造:
使用emplace_back插入左值和右值。
对于左值,仍然需要深拷贝。
对于右值,则仅调用了构造函数,不用进行拷贝构造而发生深拷贝。
emplace_back相比于push_back少调用了拷贝构造,没有进行深拷贝,大大提高了效率,降低了系统开销。
对于不存在移动构造的情况下,emplace相关接口比push_back高效很多。
所以以后用push_back系列的时候都可以用emplace_back系列替代。(想到就行)
2. lambda表达式(匿名函数)
2.1 C++11之前函数的缺陷
在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法。
#include <algorithm> #include <functional> int main() { int arr[] = { 4,1,8,5,3,7,0,9,2,6 }; // 默认按照小于比较,排出来结果是升序 std::sort(arr, arr + sizeof(arr) / sizeof(arr[0])); // 如果需要降序,需要改变元素的比较规则 std::sort(arr, arr + sizeof(arr) / sizeof(arr[0]), greater<int>()); return 0; }
如果待排序元素为自定义类型,需要用户定义排序时的比较规则:
struct Goods { string _name; // 名字 double _price; // 价格 int _evaluate; // 评价 Goods(const char* str, double price, int evaluate) :_name(str) , _price(price) , _evaluate(evaluate) {} }; struct ComparePriceLess { bool operator()(const Goods& gl, const Goods& gr) { return gl._price < gr._price; } }; struct ComparePriceGreater { bool operator()(const Goods& gl, const Goods& gr) { return gl._price > gr._price; } }; int main() { vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 7 }, { "西瓜", 5.5,3 }, { "橘子", 7.7, 4 } }; sort(v.begin(), v.end(), ComparePriceLess()); sort(v.begin(), v.end(), ComparePriceGreater()); }
随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法, 都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,可能导致命名不规范, 这些都给编程者带来了极大的不便。因此,在C++11语法中出现了lambda表达式。
从C语言到C++_34(C++11_下)可变参数+ lambda+function+bind+笔试题(中):https://developer.aliyun.com/article/1522408