牛天下网站建设,自己免费怎么做网站,wordpress 发帖机,icp许可证个人网站 #x1f4dd;个人主页#xff1a;Sherry的成长之路 #x1f3e0;学习社区#xff1a;Sherry的成长之路#xff08;个人社区#xff09; #x1f4d6;专栏链接#xff1a;C学习 #x1f3af;长路漫漫浩浩#xff0c;万事皆有期待 上一篇博客#xff1a;【C】STL… 个人主页Sherry的成长之路 学习社区Sherry的成长之路个人社区 专栏链接C学习 长路漫漫浩浩万事皆有期待 上一篇博客【C】STL详解九—— set、map、multiset、multimap的介绍及使用 文章目录 可变参数模板的概念可变参数模板的定义方式参数包的展开方式递归展开参数包逗号表达式展开参数包 STL容器中的emplace相关接口函数总结 可变参数模板的概念
可变参数模板是C11新增的最强大的特性之一它对参数高度泛化能够让我们创建可以接受可变参数的函数模板和类模板。 在C11之前类模板和函数模板中只能包含固定数量的模板参数可变模板参数无疑是一个巨大的改进但由于可变参数模板比较抽象因此使用起来需要一定的技巧。 在C11之前其实也有可变参数的概念比如printf函数就能够接收任意多个参数但这是函数参数的可变参数并不是模板的可变参数。 说明本篇博客只讲解函数模板的可变参数。
可变参数模板的定义方式
函数的可变参数模板定义方式如下
templateclass …Args
返回类型 函数名(Args… args)
{//函数体
}例如
templateclass ...Args
void ShowList(Args... args)
{}说明一下 模板参数Args前面有省略号代表它是一个可变模板参数我们把带省略号的参数称为参数包参数包里面可以包含0到N ( N ≥ 0 ) N(N\geq 0)N(N≥0)个模板参数而args则是一个函数形参参数包。 模板参数包Args和函数形参参数包args的名字可以任意指定并不是说必须叫做Args和args。 现在调用ShowList函数时就可以传入任意多个参数了并且这些参数可以是不同类型的。比如
int main()
{ShowList();ShowList(1);ShowList(1, A);ShowList(1, A, string(hello));return 0;
}我们可以在函数模板中通过sizeof计算参数包中参数的个数。比如
templateclass ...Args
void ShowList(Args... args)
{cout sizeof...(args) endl; //获取参数包中参数的个数
}但是我们无法直接获取参数包中的每个参数只能通过展开参数包的方式来获取这是使用可变参数模板的一个主要特点也是最大的难点。
特别注意语法并不支持使用args[i]的方式来获取参数包中的参数。比如
templateclass ...Args
void ShowList(Args... args)
{//错误示例for (int i 0; i sizeof...(args); i){cout args[i] ; //打印参数包中的每个参数}cout endl;
}因此要获取参数包中的各个参数只能通过展开参数包的方式来获取一般我们会通过递归或逗号表达式来展开参数包。
参数包的展开方式
递归展开参数包
递归展开参数包的方式如下 给函数模板增加一个模板参数这样就可以从接收到的参数包中分离出一个参数出来。 在函数模板中递归调用该函数模板调用时传入剩下的参数包。 如此递归下去每次分离出参数包中的一个参数直到参数包中的所有参数都被取出来。 比如我们要打印调用函数时传入的各个参数那么函数模板可以这样编写
//展开函数
templateclass T, class ...Args
void ShowList(T value, Args... args)
{cout value ; //打印分离出的第一个参数ShowList(args...); //递归调用将参数包继续向下传
}这时我们面临的问题就是如何终止函数的递归调用。 编写无参的递归终止函数 我们可以在刚才的基础上再编写一个无参的递归终止函数该函数的函数名与展开函数的函数名相同。如下
//递归终止函数
void ShowList()
{cout endl;
}
//展开函数
templateclass T, class ...Args
void ShowList(T value, Args... args)
{cout value ; //打印分离出的第一个参数ShowList(args...); //递归调用将参数包继续向下传
}这样一来当递归调用ShowList函数模板时如果传入的参数包中参数的个数为0那么就会匹配到这个无参的递归终止函数这样就结束了递归。 但如果外部调用ShowList函数时就没有传入参数那么就会直接匹配到无参的递归终止函数。 而我们本意是想让外部调用ShowList函数时匹配的都是函数模板并不是让外部调用时直接匹配到这个递归终止函数。 鉴于此我们可以将展开函数和递归调用函数的函数名改为ShowListArg然后重新编写一个ShowList函数模板该函数模板的函数体中要做的就是调用ShowListArg函数展开参数包。比如
//递归终止函数
void ShowListArg()
{cout endl;
}
//展开函数
templateclass T, class ...Args
void ShowListArg(T value, Args... args)
{cout value ; //打印传入的若干参数中的第一个参数ShowListArg(args...); //将剩下参数继续向下传
}
//供外部调用的函数
templateclass ...Args
void ShowList(Args... args)
{ShowListArg(args...);
}这时无论外部调用时传入多少个参数最终匹配到的都是同一个函数了。 编写带参的递归终止函数 除了编写无参的递归终止函数也可以编写带参数的递归终止函数来终止递归比如这里编写带一个参数的递归终止函数
//递归终止函数
templateclass T
void ShowListArg(const T t)
{cout t endl;
}
//展开函数
templateclass T, class ...Args
void ShowListArg(T value, Args... args)
{cout value ; //打印传入的若干参数中的第一个参数ShowList(args...); //将剩下参数继续向下传
}
//供外部调用的函数
templateclass ...Args
void ShowList(Args... args)
{ShowListArg(args...);
}这样一来在递归调用过程中如果传入的参数包中参数的个数为1那么就会匹配到这个递归终止函数这样也就结束了递归。但是需要注意这里的递归调用函数需要写成函数模板因为我们并不知道最后一个参数是什么类型的。
但该方法有一个弊端就是我们在调用ShowList函数时必须至少传入一个参数否则就会报错。因为此时无论是调用递归终止函数还是展开函数都需要至少传入一个参数。 判断参数包中参数的个数不可行 既然我们可以通过sizeof计算出参数包中参数的个数那我们能不能在ShowList函数中设置一个判断当参数包中参数个数为0时就终止递归呢比如
//错误示例
templateclass T, class ...Args
void ShowList(T value, Args... args)
{cout value ; //打印传入的若干参数中的第一个参数if (sizeof...(args) 0){return;}ShowList(args...); //将剩下参数继续向下传
}这种方式是不可行的原因如下 函数模板并不能调用函数模板需要在编译时根据传入的实参类型进行推演生成对应的函数这个生成的函数才能够被调用。 而这个推演过程是在编译时进行的当推演到参数包args中参数个数为0时还需要将当前函数推演完毕这时就会继续推演传入0个参数时的ShowList函数此时就会产生报错因为ShowList函数要求至少传入一个参数。 这里编写的if判断是在代码编译结束后运行代码时才会所走的逻辑也就是运行时逻辑而函数模板的推演是一个编译时逻辑。 逗号表达式展开参数包 通过列表获取参数包中的参数 数组可以通过列表进行初始化比如
int a[] {1,2,3,4}除此之外如果参数包中各个参数的类型都是整型那么也可以把这个参数包放到列表当中初始化这个整型数组此时参数包中参数就放到数组中了。比如
//展开函数
templateclass ...Args
void ShowList(Args... args)
{int arr[] { args... }; //列表初始化//打印参数包中的各个参数for (auto e : arr){cout e ;}cout endl;
}这时调用ShowList函数时就可以传入多个整型参数了。比如
int main()
{ShowList(1);ShowList(1, 2);ShowList(1, 2, 3);return 0;
}但C并不像Python这样的语言C规定一个容器中存储的数据类型必须是相同的因此如果这样写的话那么调用ShowList函数时传入的参数只能是整型的并且还不能传入0个参数因为数组的大小不能为0因此我们还需要在此基础上借助逗号表达式来展开参数包。 通过逗号表达式展开参数包 虽然我们不能用不同类型的参数去初始化一个整型数组但我们可以借助逗号表达式。 逗号表达式会从左到右依次计算各个表达式并且将最后一个表达式的值作为返回值进行返回。 将逗号表达式的最后一个表达式设置为一个整型值确保逗号表达式返回的是一个整型值。 将处理参数包中参数的动作封装成一个函数将该函数的调用作为逗号表达式的第一个表达式。 这样一来在执行逗号表达式时就会先调用处理函数处理对应的参数然后再将逗号表达式中的最后一个整型值作为返回值来初始化整型数组。比如
//处理参数包中的每个参数
templateclass T
void PrintArg(const T t)
{cout t ;
}
//展开函数
templateclass ...Args
void ShowList(Args... args)
{int arr[] { (PrintArg(args), 0)... }; //列表初始化逗号表达式cout endl;
}说明一下 我们这里要做的就是打印参数包中的各个参数因此处理函数当中要做的就是将传入的参数进行打印即可。 可变参数的省略号需要加在逗号表达式外面表示需要将逗号表达式展开如果将省略号加在args的后面那么参数包将会被展开后全部传入PrintArg函数代码中的{(PrintArg(args), 0)…}将会展开成{(PrintArg(arg1), 0), (PrintArg(arg2), 0), (PrintArg(arg3), 0), etc…}。 这时调用ShowList函数时就可以传入多个不同类型的参数了但调用时仍然不能传入0个参数因为数组的大小不能为0如果想要支持传入0个参数也可以写一个无参的ShowList函数。比如
//支持无参调用
void ShowList()
{cout endl;
}
//处理函数
templateclass T
void PrintArg(const T t)
{cout t ;
}
//展开函数
templateclass ...Args
void ShowList(Args... args)
{int arr[] { (PrintArg(args), 0)... }; //列表初始化逗号表达式cout endl;
}实际上我们也可以不用逗号表达式因为这里的问题就是初始化整型数组时必须用整数那我们可以将处理函数的返回值设置为整型然后用这个返回值去初始化整型数组也是可以的。比如
//支持无参调用
void ShowList()
{cout endl;
}
//处理函数
templateclass T
int PrintArg(const T t)
{cout t ;return 0;
}
//展开函数
templateclass ...Args
void ShowList(Args... args)
{int arr[] { PrintArg(args)... }; //列表初始化cout endl;
}STL容器中的emplace相关接口函数 emplace版本的插入接口 C11标准给STL中的容器增加emplace版本的插入接口比如list容器的push_front、push_back和insert函数都增加了对应的emplace_front、emplace_back和emplace函数。如下
这些emplace版本的插入接口支持模板的可变参数比如list容器的emplace_back函数的声明如下
注意 emplace系列接口的可变模板参数类型都带有“”这个表示的是万能引用而不是右值引用。 emplace系列接口的使用方式 emplace系列接口的使用方式与容器原有的插入接口的使用方式类似但又有一些不同之处。
以list容器的emplace_back和push_back为例 调用push_back函数插入元素时可以传入左值对象或者右值对象也可以使用列表进行初始化。 调用emplace_back函数插入元素时也可以传入左值对象或者右值对象但不可以使用列表进行初始化。 除此之外emplace系列接口最大的特点就是插入元素时可以传入用于构造元素的参数包。 比如
int main()
{listpairint, string mylist;pairint, string kv(10, 111);mylist.push_back(kv); //传左值mylist.push_back(pairint, string(20, 222)); //传右值mylist.push_back({ 30, 333 }); //列表初始化mylist.emplace_back(kv); //传左值mylist.emplace_back(pairint, string(40, 444)); //传右值mylist.emplace_back(50, 555); //传参数包return 0;
}emplace系列接口的工作流程 emplace系列接口的工作流程如下 先通过空间配置器为新结点获取一块内存空间注意这里只会开辟空间不会自动调用构造函数对这块空间进行初始化。 然后调用allocator_traits::construct函数对这块空间进行初始化调用该函数时会传入这块空间的地址和用户传入的参数需要经过完美转发。 在allocator_traits::construct函数中会使用定位new表达式显示调用构造函数对这块空间进行初始化调用构造函数时会传入用户传入的参数需要经过完美转发。 将初始化好的新结点插入到对应的数据结构当中比如list容器就是将新结点插入到底层的双链表中。 emplace系列接口的意义 由于emplace系列接口的可变模板参数的类型都是万能引用因此既可以接收左值对象也可以接收右值对象还可以接收参数包。 如果调用emplace系列接口时传入的是左值对象那么首先需要先在此之前调用构造函数实例化出一个左值对象最终在使用定位new表达式调用构造函数对空间进行初始化时会匹配到拷贝构造函数。 如果调用emplace系列接口时传入的是右值对象那么就需要在此之前调用构造函数实例化出一个右值对象最终在使用定位new表达式调用构造函数对空间进行初始化时就会匹配到移动构造函数。 如果调用emplace系列接口时传入的是参数包那就可以直接调用函数进行插入并且最终在使用定位new表达式调用构造函数对空间进行初始化时匹配到的是构造函数。
总结一下 传入左值对象需要调用构造函数拷贝构造函数。 传入右值对象需要调用构造函数移动构造函数。 传入参数包只需要调用构造函数。
当然这里的前提是容器中存储的元素所对应的类是一个需要深拷贝的类并且该类实现了移动构造函数。否则调用emplace系列接口时传入左值对象和传入右值对象的效果都是一样的都需要调用一次构造函数和一次拷贝构造函数。
实际emplace系列接口的一部分功能和原有各个容器插入接口是重叠的因为容器原有的push_back、push_front和insert函数也提供了右值引用版本的接口如果调用这些接口时如果传入的是右值对象那么最终也是会调用对应的移动构造函数进行资源的移动的。
emplace接口的意义
emplace系列接口最大的特点就是支持传入参数包用这些参数包直接构造出对象这样就能减少一次拷贝这就是为什么有人说emplace系列接口更高效的原因。 但emplace系列接口并不是在所有场景下都比原有的插入接口高效如果传入的是左值对象或右值对象那么emplace系列接口的效率其实和原有的插入接口的效率是一样的。 emplace系列接口真正高效的情况是传入参数包的时候直接通过参数包构造出对象避免了中途的一次拷贝。 验证 如果要验证我们上述对emplace系列接口的说法需要借助一个深拷贝的类下面模拟实现了一个简化版的string类类当中只编写了我们需要用到的成员函数。
代码如下
namespace sherry
{class string{public://构造函数string(const char* str ){cout string(const char* str) -- 构造函数 endl;_size strlen(str); //初始时字符串大小设置为字符串长度_capacity _size; //初始时字符串容量设置为字符串长度_str new char[_capacity 1]; //为存储字符串开辟空间多开一个用于存放\0strcpy(_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拷贝构造出对象tmpswap(tmp); //交换这两个对象return *this; //返回左值支持连续赋值}//移动赋值string operator(string s){cout string operator(string s) -- 移动赋值 endl;swap(s);return *this;}//析构函数~string(){//delete[] _str; //释放_str指向的空间_str nullptr; //及时置空防止非法访问_size 0; //大小置0_capacity 0; //容量置0}private:char* _str;size_t _size;size_t _capacity;};
}
由于我们在string的构造函数、拷贝构造函数和移动构造函数当中均打印了一条提示语句因此我们可以通过控制台输出来判断这些函数是否被调用。
下面我们用一个容器来存储模拟实现的string并以不同的传参形式调用emplace系列函数。比如
int main()
{listpairint, cl::string mylist;pairint, cl::string kv(1, one);mylist.emplace_back(kv); //传左值cout endl;mylist.emplace_back(pairint, cl::string(2, two)); //传右值cout endl;mylist.emplace_back(3, three); //传参数包return 0;
}运行结果如下
说明一下
模拟实现string的拷贝构造函数时复用了构造函数因此在调用string拷贝构造的后面会紧跟着调用一次构造函数。 为了更好的体现出参数包的概念因此这里list容器中存储的元素类型是pair我们是通过观察string对象的处理过程来判断pair的处理过程的。
这里也可以以不同的传参方式调用push_back函数顺便验证一下容器原有的插入函数的执行逻辑。比如
int main()
{listpairint, cl::string mylist;pairint, cl::string kv(1, one);mylist.push_back(kv); //传左值cout endl;mylist.push_back(pairint, cl::string(2, two)); //传右值cout endl;mylist.push_back({ 3, three }); //列表初始化return 0;
}运行结果如下
总结
今天我们学习了C11中的可变参数模板了解了一些有关的底层原理。接下来我们将继续进行C11的学习。希望我的文章和讲解能对大家的学习提供一些帮助。 当然本文仍有许多不足之处欢迎各位小伙伴们随时私信交流、批评指正我们下期见~