1.C语言方式
目录
1.1.宏介绍
C语言中的可变参数是指函数可以接受可变数量的参数。这些参数的数量在编译时是未知的。在这些可变参数中的参数类型可以相同,也可以不同;可变参数的每个参数并没有实际的名称与之相对应,用起来是很灵活;在头文件stdarg.h中,涉及到的宏有:
va_list : 是指向参数的指针 ,通过指针运算来调整访问的对象
va_start :获取可变参数列表的第一个参数的地址
va_arg : 获取可变参数的当前参数,返回指定类型并将指针指向下一参数
va_end : 清空va_list可变参数列表
1.2.原理详解
函数的参数是存放在栈中,地址是连续的,所以可以通过相对位置去访问,这也是可变参数的访问方式;变长参数的实现需要依赖于C语言默认的cdecl调用惯例的自右向左压栈传递方式;可变参数是由1.1介绍的几个宏来实现,但是由于硬件平台的不同,编译器的不同,宏的定义也不相同,下面是AMD CPU x64平台下的定义:
typedef char* va_list;
va_list的定义
- //[1]
- #ifdef __cplusplus
- #define _ADDRESSOF(v) (&reinterpret_cast
(v)) - #else
- #define _ADDRESSOF(v) (&(v))
- #endif
-
- //[2]
- #define va_start _crt_va_start
- #define va_arg _crt_va_arg
- #define va_end _crt_va_end
- #define va_copy(destination, source) ((destination) = (source))
-
- //[3]
- #define _PTRSIZEOF(n) ((sizeof(n) + sizeof(void*) - 1) & ~(sizeof(void*) - 1))//系统内存对齐
- #define _ISSTRUCT(t) ((sizeof(t) > sizeof(void*)) || (sizeof(t) & (sizeof(t) - 1)) != 0)
- #define _crt_va_start(v,l) ((v) = (va_list)_ADDRESSOF(l) + _PTRSIZEOF(l))
- #define _crt_va_arg(v,t) _ISSTRUCT(t) ? \
- (**(t**)(((v) += sizeof(void*)) - sizeof(void*))) : \
- ( *(t *)(((v) += sizeof(void*)) - sizeof(void*)))
- #define _crt_va_end(v) ((v) = (va_list)0)
- #define _crt_va_copy(d,s) ((d) = (s))
从上面的源码可以看出:
1) va_list v; 定义一个指向char类型的指针v。
2) va_start(v,l) ;执行 v = (va_list)&l + _PTRSIZEOF(l) ,v指向参数 l 之后的那个参数的地址,即 v指向第一个可变参数在堆栈的地址。
3) va_arg(v,t) , ( (t )((v += _PTRSIZEOF(t)) - _PTRSIZEOF(t)) ) 取出当前v指针所指的值,并使 v 指向下一个参数。 v+=sizeof(t类型) ,让v指向下一个参数的地址。然后返回 v - sizeof(t类型) 的t类型指针,这正是第一个可变参数在堆栈里的地址。然后 用取得这个地址的内容。
va_end(v) ; 清空 va_list v。
备注:_PTRSIZEOF(n) 是怎么做到内存对齐的呢?
sizeof(void*)肯定是的2的整数倍,如果是32位的系统就是4,64位的系统就是8。以32位的系统为例, A) sizeof(void*)=4 二进制是 0000 0100
B) sizeof(void*)-1=3 二进制是 0000 0011
C)~(sizeof(void*) - 1)是在第二步的基础上取反,二进制是 1111 1100
1) 当n传入int,等于4个字节,D = sizeof(int)=4,D+A = 0000 0111 , 再和C与一下,把高位和地位都清零了,算出结果 0000 0100=4,正好是32位系统对齐的字节数。
2) 当传入short,低于4个字节,D = sizeof(short)=2,D+A = 0000 110 , 再和C与一下,把高位和地位都清零了,算出结果 0000 0100=4,正好是32位系统对齐的字节数。
3)当传入double, 高于4个字节, D = sizeof(double)=8,D+A = 0001 011 ,再和C与一下,把高位和地位都清零了,算出结果 0000 1000=8,正好是double对齐的字节数。
由此可见,32位系统_PTRSIZEOF(n)小于等于4字节时,就按照4字节对齐,大于4字节时,就按照sizeof(n)对齐。
可以用此方法分析64位系统用_PTRSIZEOF(n)计算的字节对齐数,一样的原理。
1.3.宏的可变参数
标准C/C++语言宏定义的参数允许用三个小数点 ...
表示这里是可变参数,在宏替换的时候,用 __VA_ARGS__
表示 ...
位置的所有的参数,例如:
- #define example1(...) printf(__VA_ARGS__)
- #define example2(fmt, ...) printf(fmt, __VA_ARGS__)
很多编译器扩展了可变参数的宏替换,参数后面带三个小数点,这样的写法更容易记忆,宏定义的参数后面可以带三个小数点,表示这里是可变参数,宏替换的时候,直接写这个参数就表示这个位置是所有的可变参数了。例如:
- #define example1(fmt...) printf(fmt)
- #define example2(fmt, args...) printf(fmt, args)
1.4.案例分析
- #include
- #include
-
- void printValues(const char* format, ...) {
- va_list args; // 定义一个va_list类型的变量
- va_start(args, format); // 初始化args
-
- for (const char* arg = format; *arg != '\0'; ++arg) {
- if (*arg == '%') {
- ++arg;
- switch (*arg) {
- case 'd': // 对于整数
- std::cout << va_arg(args, int);
- break;
- case 's': // 对于字符串
- std::cout << va_arg(args, char*);
- break;
- default:
- std::cout << "Invalid format specifier: " << *arg;
- }
- }
- else {
- std::cout << *arg;
- }
- }
- va_end(args); // 清理va_list变量
- }
-
- int main() {
- printValues("say self info: %s, age %d\n", "xiao", 45); //输出: say self info xiao, age 45
- return 0;
- }
printValues函数调用的时候展开为:
void printValues(const char* format, const char* param1, int param2)
从上面的代码来分析一下这个示例:在windows中,栈由高地址往低地址生长,调用printValues函数时,其参数入栈情况如下:
当调用va_start(args, format)时:args指针指向情况对应下图:
当调用va_arg(args, ...)时,它必须返回一个由va_list所指向的恰当的类型的数值,同时递增args,使它指向参数列表中的一个参数(即递增的大小等于与va_arg宏所返回的数值具有相同类型的对象的长度)。因为类型转换的结果不能作为赋值运算的目标,所以va_arg宏首先使用sizeof来确定需要递增的大小,然后把它直接加到va_list上,这样得到的指针再被转换为要求的类型。
在上面的示例中,我们定义了一个名为printValues的函数,它接受一个格式字符串和一个可变数量的参数。我们使用va_list、va_start、va_arg和va_end这些宏来处理可变参数。在格式字符串中,我们使用%来指定参数的类型,例如%d表示整数,%s表示字符串。然后,我们使用va_arg宏来获取相应的参数值。最后,我们使用va_end宏来清理va_list变量。
1.5.其他实例
1) printf实现
- #include
-
- int printf(char *format, ...)
- {
- va_list ap;
- int n;
-
- va_start(ap, format);
- n = vprintf(format, ap);
- va_end(ap);
- return n;
- }
2)定制错误打印函数error
- #include
- #include
-
- void error(char *format, ...)
- {
- va_list ap;
- va_start(ap, format);
- fprintf(stderr, "Error: ");
- vfprintf(stderr, format, ap);
- va_end(ap);
- fprintf(stderr, "\n");
- return;
- }
2.C++之std::initializer_list
在C++中我们一般用()和=初始化参数或对象,还可以用{}来初始化参数或对象,比如数组的初始化int m[] = {1,4,5},除了数组,在STL里面很多标准的容器和自定义类型都用{} 进行初始化。
自C++11标准开始就引入了列表初始化的概念,即支持使用{}对变量或对象进行初始化,且与传统的变量初始化的规则一样,也分为拷贝初始化和直接初始化两种方式。
2.1.简介
std::initializer_list
std::initializer_list 对象在这些时候自动构造:
1)用花括号初始化器列表列表初始化一个对象,其中对应构造函数接受一个 std::initializer_list 参数,如std::vector的构造函数 vector(initializer_list<_Ty> _Ilist, const _Alloc& _Al = _Alloc())
2)以花括号初始化器列表为赋值的右运算数,或函数调用参数,而对应的赋值运算符/函数接受 std::initializer_list 参数
3)绑定花括号初始化器列表到 auto ,包括在范围 for 循环中
initializer_list 可由一对指针或指针与其长度实现。复制一个 std::initializer_list 不会复制其底层对象。
注意:
a、底层数组不保证在原始 initializer_list 对象的生存期结束后继续存在。 std::initializer_list
的存储是未指定的(即它可以是自动、临时或静态只读内存,依赖场合)。
b、底层数组是 const T[N] 类型的临时数组,其中每个元素都从原始初始化器列表的对应元素复制初始化(除非窄化转换非法)。底层数组的生存期与任何其他临时对象相同,除了从数组初始化 initializer_list 对象会延长数组的生存期,恰如绑定引用到临时量(有例外,例如对于初始化非静态类成员)。底层数组可以分配在只读内存。
c、若声明了 std::initializer_list
的显式或偏特化则程序为谬构。
2.2.原理详解
源码面前无秘密,直接上源码:
- template <class _Elem>
- class initializer_list {
- public:
- using value_type = _Elem;
- using reference = const _Elem&;
- using const_reference = const _Elem&;
- using size_type = size_t;
-
- using iterator = const _Elem*;
- using const_iterator = const _Elem*;
-
- constexpr initializer_list() noexcept : _First(nullptr), _Last(nullptr) {} //1
-
- constexpr initializer_list(const _Elem* _First_arg, const _Elem* _Last_arg) noexcept
- : _First(_First_arg), _Last(_Last_arg) {} //2
-
- _NODISCARD constexpr const _Elem* begin() const noexcept {
- return _First;
- }
-
- _NODISCARD constexpr const _Elem* end() const noexcept {
- return _Last;
- }
-
- _NODISCARD constexpr size_t size() const noexcept {
- return static_cast<size_t>(_Last - _First);
- }
-
- private:
- const _Elem* _First;
- const _Elem* _Last;
- };
-
- // FUNCTION TEMPLATE begin
- template <class _Elem>
- _NODISCARD constexpr const _Elem* begin(initializer_list<_Elem> _Ilist) noexcept {
- return _Ilist.begin();
- }
-
- // FUNCTION TEMPLATE end
- template <class _Elem>
- _NODISCARD constexpr const _Elem* end(initializer_list<_Elem> _Ilist) noexcept {
- return _Ilist.end();
- }
从上面的STL的std::initializer_list源码来看,std::initializer_list是一个模版类,定义了指向该类对象首端、尾端的迭代器(即常量对象指针const T*),实际上就是对{}表达式内容的简单封装,当使用{}时,就会调用 initializer_list(const _Elem* _First_arg, const _Elem* _Last_arg) 构造出std::initializer_list。
当得到了一个std::initializer_list对象后,再来寻找标准容器中以std::initializer_list为形参的构造函数,并调用该构造函数对容器进行初始化。
2.3.案例分析
示例1:
- class IMessageField1 {};
-
- //1
- void addMessageField(std::initializer_list
t) - {
- std::vector
pTest(t) ; - }
-
- #if 0
- //2
- void addMessageField(std::vector
t) - {
- std::vector
pTest(t) ; - }
- #endif
-
- void main()
- {
- //[1]
- std::unique_ptr
a(new IMessageField1) ; - std::unique_ptr
b(new IMessageField1) ; - std::unique_ptr
c(new IMessageField1) ; - std::unique_ptr
d(new IMessageField1) ; - std::unique_ptr
e(new IMessageField1) ; - addMessageField({ a.get(), b.get(), c.get(), d.get(), e.get() });
- }
上面代码1和2的方式都可以实现功能,2的方式实际上也是先临时生成一个std::initializer_list,再调用std::vector的构造函数临时生成一个std::vector,最后再用刚生成的std::vector初始化pTest,相比1的方式,多了几重复制,效率比较低,一般采用1的方式实现功能。
示例2:
- #include
- #include
- #include
-
- template <class T>
- struct S {
- std::vector
v; - S(std::initializer_list
l) : v(l) { - std::cout << "constructed with a " << l.size() << "-element list\n";
- }
- void append(std::initializer_list
l) { - v.insert(v.end(), l.begin(), l.end());
- }
- std::pair<const T*, std::size_t> c_arr() const {
- return {&v[0], v.size()}; // 在 return 语句中复制列表初始化
- // 这不使用 std::initializer_list
- }
- };
-
- template <typename T>
- void templated_fn(T) {}
-
- int main()
- {
- int a1[] = { 1,2,3,4,5,6 }; //数组拷贝初始化
- int a2[]{ 5,6,7,8,9,0 }; //数组直接初始化
-
- S<int> s = {1, 2, 3, 4, 5}; // 复制初始化
- s.append({6, 7, 8}); // 函数调用中的列表初始化
-
- std::cout << "The vector size is now " << s.c_arr().second << " ints:\n";
-
- for (auto n : s.v)
- std::cout << n << ' ';
- std::cout << '\n';
-
- std::cout << "Range-for over brace-init-list: \n";
-
- for (int x : {-1, -2, -3}) // auto 的规则令此带范围 for 工作
- std::cout << x << ' ';
- std::cout << '\n';
-
- auto al = {10, 11, 12}; // auto 的特殊规则
-
- std::cout << "The list bound to auto has size() = " << al.size() << '\n';
-
- // templated_fn({1, 2, 3}); // 编译错误!“ {1, 2, 3} ”不是表达式,
- // 它无类型,故 T 无法推导
- templated_fn
int>>({1, 2, 3}); // OK - templated_fn
int>>({1, 2, 3}); // 也 OK - }
输出:
- constructed with a 5-element list
- The vector size is now 8 ints:
- 1 2 3 4 5 6 7 8
- Range-for over brace-init-list:
- -1 -2 -3
- The list bound to auto has size() = 3
示例3:
- struct MyTest{
- explicit X(int a, int b) :a(a), b(b) { std::cout << "MyTest(int a,int b)\n"; }
-
- int a{};
- int b{};
- };
-
- int main() {
- MyTest x{ 1,2 }; //OK
- MyTest x2( 1,2 ); //OK
- MyTest x3 = { 1,2 }; //Error
- }
MyTest x3 ={1,2};
参考复制初始化的规则:复制列表初始化(考虑 explicit 和非 explicit 构造函数,但只能调用非 explicit 构造函数)
3.C++之可变参数模版
3.1.简介
一个可变参数模板就是一个可以接受可变参数的模版函数或模板类;参数的类型是一种模板,是可经推导的,可以是任意存在的类型(系统类型或自定义类型);参数数目可变的,可以包括零个、一个或多个;可变数目的参数被称为参数包(parameter packet)。存在两种参数包:模板参数包(template parameter packet),表示零个或多个模板参数;函数参数包function parameterpacket),表示零个或多个函数参数。如:
- template<typename... Arguments> class vtclass;
-
- vtclass< > vtinstance1;
- vtclass<int> vtinstance2;
- vtclass<float, bool> vtinstance3;
- vtclass<long, std::vector<int>, std::string> vtinstance4;
3.2.可变参数个数
利用sizeof...()计算可变参数的大小,如:
- template<class... Types>
- struct count
- {
- static const std::size_t value = sizeof...(Types);
- };
3.3.递归包展开
C++的包展开是通过 args...
的形式,后面...
就意味着展开包,需要两个函数:递归终止函数 和 递归函数,过程就是参数包在展开的过程中递归调用自己,每调用一次参数包中的参数就会少一个,直到所有的参数都展开为止,当没有参数时,则调用递归终止函数终止递归过程。如下:
- #include
-
- using namespace std;
-
- void print() {
- cout << endl;
- }
-
- template <typename T>
- void print(const T& t) { //边界条件
- cout << t << endl;
- }
-
- template <typename First, typename... Rest>
- void print(const First& first, const Rest&... rest) {
- cout << first << ", ";
- print(rest...); //打印剩余参数,注意省略号必须有
- }
-
- int main()
- {
- print(); // calls first overload, outputting only a newline
- print(1); // calls second overload
-
- // these call the third overload, the variadic template,
- // which uses recursion as needed.
- print(10, 20); //输出: 10, 20
- print(100, 200, 300); //输出:100, 200, 300
- print("first", 2, "third", 3.14159); //输出: first, 2, third, 3.14159
- }
3.4.逗号表达式展开
逗号表达式是会从左到右依次计算各个表达式,并将最后一个表达式的值作为返回值返回;我们将最后一个表达式设为整型值,所以最后返回的是一个整型;将处理参数个数的动作封装成一个函数,将该函数作为逗号表达式的第一个表达式;…代表参数包,列表展开。如:
- template <class T>
- void printArg(T t) {
- cout << t << endl;
- }
-
- //展开参数包
- template <class ...Args>
- void expand(Args... args) {
- int arr[] = { (printArg(args), 0)... };
- }
- int main()
- {
- expand(1);
- expand(1, 'A');
- expand(1, "hello", 3);
- return 0;
- }
函数执行expand(1, "hello", 3);的时候,调用expand,数组arr初始化会展开args参数,变化为:
int arr[] = {(printArg(1), 0), (printArg("hello"), 0), (printArg(3), 0)};
根据逗号表达式的规则,arr[] 还是 {0,0,0};
另外,还可以利用std::initializer_list 可以接收任意长度的初始化列表来展开包,如:
- template<class F, class... Args>
- void expand(const F& f, Args&&...args) {
- std::initializer_list<int>{(f(std::forward< Args>(args)), 0)...};
- }
-
- int main()
- {
- expand([](int i) { cout << i << endl; }, 23, 44, 2423);
- return 0;
- }
3.5.Lambda 捕获
包展开可以在 lambda 表达式的捕获子句中出现:
- template<class... Args>
- void f(Args... args)
- {
- auto lm = [&, args...] { return g(args...); };
- lm();
- }
3.6.转发参数包
在C++11标准下,我们可以组合使用可变参数模板与std::forword机制来编写函数,实现将其实参不变地传递给其他函数,关于std::forward的详解讲解,可参考我的博客:C++之std::forward_c++ std::forward-CSDN博客
借助std::forward
就可以实现参数的完美转发了,如STL中map的插入函数emplace下:
- template <class... _Valty>
- iterator emplace(_Valty&&... _Val)
- {
- return _Mybase::emplace(_STD forward<_Valty>(_Val)...).first;
- }
4.总结
纸上得来终觉浅,绝知此事要躬行。
参考
1.C语言方式
目录
1.1.宏介绍
C语言中的可变参数是指函数可以接受可变数量的参数。这些参数的数量在编译时是未知的。在这些可变参数中的参数类型可以相同,也可以不同;可变参数的每个参数并没有实际的名称与之相对应,用起来是很灵活;在头文件stdarg.h中,涉及到的宏有:
va_list : 是指向参数的指针 ,通过指针运算来调整访问的对象
va_start :获取可变参数列表的第一个参数的地址
va_arg : 获取可变参数的当前参数,返回指定类型并将指针指向下一参数
va_end : 清空va_list可变参数列表
1.2.原理详解
函数的参数是存放在栈中,地址是连续的,所以可以通过相对位置去访问,这也是可变参数的访问方式;变长参数的实现需要依赖于C语言默认的cdecl调用惯例的自右向左压栈传递方式;可变参数是由1.1介绍的几个宏来实现,但是由于硬件平台的不同,编译器的不同,宏的定义也不相同,下面是AMD CPU x64平台下的定义:
typedef char* va_list;
va_list的定义
- //[1]
- #ifdef __cplusplus
- #define _ADDRESSOF(v) (&reinterpret_cast
(v)) - #else
- #define _ADDRESSOF(v) (&(v))
- #endif
-
- //[2]
- #define va_start _crt_va_start
- #define va_arg _crt_va_arg
- #define va_end _crt_va_end
- #define va_copy(destination, source) ((destination) = (source))
-
- //[3]
- #define _PTRSIZEOF(n) ((sizeof(n) + sizeof(void*) - 1) & ~(sizeof(void*) - 1))//系统内存对齐
- #define _ISSTRUCT(t) ((sizeof(t) > sizeof(void*)) || (sizeof(t) & (sizeof(t) - 1)) != 0)
- #define _crt_va_start(v,l) ((v) = (va_list)_ADDRESSOF(l) + _PTRSIZEOF(l))
- #define _crt_va_arg(v,t) _ISSTRUCT(t) ? \
- (**(t**)(((v) += sizeof(void*)) - sizeof(void*))) : \
- ( *(t *)(((v) += sizeof(void*)) - sizeof(void*)))
- #define _crt_va_end(v) ((v) = (va_list)0)
- #define _crt_va_copy(d,s) ((d) = (s))
从上面的源码可以看出:
1) va_list v; 定义一个指向char类型的指针v。
2) va_start(v,l) ;执行 v = (va_list)&l + _PTRSIZEOF(l) ,v指向参数 l 之后的那个参数的地址,即 v指向第一个可变参数在堆栈的地址。
3) va_arg(v,t) , ( (t )((v += _PTRSIZEOF(t)) - _PTRSIZEOF(t)) ) 取出当前v指针所指的值,并使 v 指向下一个参数。 v+=sizeof(t类型) ,让v指向下一个参数的地址。然后返回 v - sizeof(t类型) 的t类型指针,这正是第一个可变参数在堆栈里的地址。然后 用取得这个地址的内容。
va_end(v) ; 清空 va_list v。
备注:_PTRSIZEOF(n) 是怎么做到内存对齐的呢?
sizeof(void*)肯定是的2的整数倍,如果是32位的系统就是4,64位的系统就是8。以32位的系统为例, A) sizeof(void*)=4 二进制是 0000 0100
B) sizeof(void*)-1=3 二进制是 0000 0011
C)~(sizeof(void*) - 1)是在第二步的基础上取反,二进制是 1111 1100
1) 当n传入int,等于4个字节,D = sizeof(int)=4,D+A = 0000 0111 , 再和C与一下,把高位和地位都清零了,算出结果 0000 0100=4,正好是32位系统对齐的字节数。
2) 当传入short,低于4个字节,D = sizeof(short)=2,D+A = 0000 110 , 再和C与一下,把高位和地位都清零了,算出结果 0000 0100=4,正好是32位系统对齐的字节数。
3)当传入double, 高于4个字节, D = sizeof(double)=8,D+A = 0001 011 ,再和C与一下,把高位和地位都清零了,算出结果 0000 1000=8,正好是double对齐的字节数。
由此可见,32位系统_PTRSIZEOF(n)小于等于4字节时,就按照4字节对齐,大于4字节时,就按照sizeof(n)对齐。
可以用此方法分析64位系统用_PTRSIZEOF(n)计算的字节对齐数,一样的原理。
1.3.宏的可变参数
标准C/C++语言宏定义的参数允许用三个小数点 ...
表示这里是可变参数,在宏替换的时候,用 __VA_ARGS__
表示 ...
位置的所有的参数,例如:
- #define example1(...) printf(__VA_ARGS__)
- #define example2(fmt, ...) printf(fmt, __VA_ARGS__)
很多编译器扩展了可变参数的宏替换,参数后面带三个小数点,这样的写法更容易记忆,宏定义的参数后面可以带三个小数点,表示这里是可变参数,宏替换的时候,直接写这个参数就表示这个位置是所有的可变参数了。例如:
- #define example1(fmt...) printf(fmt)
- #define example2(fmt, args...) printf(fmt, args)
1.4.案例分析
- #include
- #include
-
- void printValues(const char* format, ...) {
- va_list args; // 定义一个va_list类型的变量
- va_start(args, format); // 初始化args
-
- for (const char* arg = format; *arg != '\0'; ++arg) {
- if (*arg == '%') {
- ++arg;
- switch (*arg) {
- case 'd': // 对于整数
- std::cout << va_arg(args, int);
- break;
- case 's': // 对于字符串
- std::cout << va_arg(args, char*);
- break;
- default:
- std::cout << "Invalid format specifier: " << *arg;
- }
- }
- else {
- std::cout << *arg;
- }
- }
- va_end(args); // 清理va_list变量
- }
-
- int main() {
- printValues("say self info: %s, age %d\n", "xiao", 45); //输出: say self info xiao, age 45
- return 0;
- }
printValues函数调用的时候展开为:
void printValues(const char* format, const char* param1, int param2)
从上面的代码来分析一下这个示例:在windows中,栈由高地址往低地址生长,调用printValues函数时,其参数入栈情况如下:
当调用va_start(args, format)时:args指针指向情况对应下图:
当调用va_arg(args, ...)时,它必须返回一个由va_list所指向的恰当的类型的数值,同时递增args,使它指向参数列表中的一个参数(即递增的大小等于与va_arg宏所返回的数值具有相同类型的对象的长度)。因为类型转换的结果不能作为赋值运算的目标,所以va_arg宏首先使用sizeof来确定需要递增的大小,然后把它直接加到va_list上,这样得到的指针再被转换为要求的类型。
在上面的示例中,我们定义了一个名为printValues的函数,它接受一个格式字符串和一个可变数量的参数。我们使用va_list、va_start、va_arg和va_end这些宏来处理可变参数。在格式字符串中,我们使用%来指定参数的类型,例如%d表示整数,%s表示字符串。然后,我们使用va_arg宏来获取相应的参数值。最后,我们使用va_end宏来清理va_list变量。
1.5.其他实例
1) printf实现
- #include
-
- int printf(char *format, ...)
- {
- va_list ap;
- int n;
-
- va_start(ap, format);
- n = vprintf(format, ap);
- va_end(ap);
- return n;
- }
2)定制错误打印函数error
- #include
- #include
-
- void error(char *format, ...)
- {
- va_list ap;
- va_start(ap, format);
- fprintf(stderr, "Error: ");
- vfprintf(stderr, format, ap);
- va_end(ap);
- fprintf(stderr, "\n");
- return;
- }
2.C++之std::initializer_list
在C++中我们一般用()和=初始化参数或对象,还可以用{}来初始化参数或对象,比如数组的初始化int m[] = {1,4,5},除了数组,在STL里面很多标准的容器和自定义类型都用{} 进行初始化。
自C++11标准开始就引入了列表初始化的概念,即支持使用{}对变量或对象进行初始化,且与传统的变量初始化的规则一样,也分为拷贝初始化和直接初始化两种方式。
2.1.简介
std::initializer_list
std::initializer_list 对象在这些时候自动构造:
1)用花括号初始化器列表列表初始化一个对象,其中对应构造函数接受一个 std::initializer_list 参数,如std::vector的构造函数 vector(initializer_list<_Ty> _Ilist, const _Alloc& _Al = _Alloc())
2)以花括号初始化器列表为赋值的右运算数,或函数调用参数,而对应的赋值运算符/函数接受 std::initializer_list 参数
3)绑定花括号初始化器列表到 auto ,包括在范围 for 循环中
initializer_list 可由一对指针或指针与其长度实现。复制一个 std::initializer_list 不会复制其底层对象。
注意:
a、底层数组不保证在原始 initializer_list 对象的生存期结束后继续存在。 std::initializer_list
的存储是未指定的(即它可以是自动、临时或静态只读内存,依赖场合)。
b、底层数组是 const T[N] 类型的临时数组,其中每个元素都从原始初始化器列表的对应元素复制初始化(除非窄化转换非法)。底层数组的生存期与任何其他临时对象相同,除了从数组初始化 initializer_list 对象会延长数组的生存期,恰如绑定引用到临时量(有例外,例如对于初始化非静态类成员)。底层数组可以分配在只读内存。
c、若声明了 std::initializer_list
的显式或偏特化则程序为谬构。
2.2.原理详解
源码面前无秘密,直接上源码:
- template <class _Elem>
- class initializer_list {
- public:
- using value_type = _Elem;
- using reference = const _Elem&;
- using const_reference = const _Elem&;
- using size_type = size_t;
-
- using iterator = const _Elem*;
- using const_iterator = const _Elem*;
-
- constexpr initializer_list() noexcept : _First(nullptr), _Last(nullptr) {} //1
-
- constexpr initializer_list(const _Elem* _First_arg, const _Elem* _Last_arg) noexcept
- : _First(_First_arg), _Last(_Last_arg) {} //2
-
- _NODISCARD constexpr const _Elem* begin() const noexcept {
- return _First;
- }
-
- _NODISCARD constexpr const _Elem* end() const noexcept {
- return _Last;
- }
-
- _NODISCARD constexpr size_t size() const noexcept {
- return static_cast<size_t>(_Last - _First);
- }
-
- private:
- const _Elem* _First;
- const _Elem* _Last;
- };
-
- // FUNCTION TEMPLATE begin
- template <class _Elem>
- _NODISCARD constexpr const _Elem* begin(initializer_list<_Elem> _Ilist) noexcept {
- return _Ilist.begin();
- }
-
- // FUNCTION TEMPLATE end
- template <class _Elem>
- _NODISCARD constexpr const _Elem* end(initializer_list<_Elem> _Ilist) noexcept {
- return _Ilist.end();
- }
从上面的STL的std::initializer_list源码来看,std::initializer_list是一个模版类,定义了指向该类对象首端、尾端的迭代器(即常量对象指针const T*),实际上就是对{}表达式内容的简单封装,当使用{}时,就会调用 initializer_list(const _Elem* _First_arg, const _Elem* _Last_arg) 构造出std::initializer_list。
当得到了一个std::initializer_list对象后,再来寻找标准容器中以std::initializer_list为形参的构造函数,并调用该构造函数对容器进行初始化。
2.3.案例分析
示例1:
- class IMessageField1 {};
-
- //1
- void addMessageField(std::initializer_list
t) - {
- std::vector
pTest(t) ; - }
-
- #if 0
- //2
- void addMessageField(std::vector
t) - {
- std::vector
pTest(t) ; - }
- #endif
-
- void main()
- {
- //[1]
- std::unique_ptr
a(new IMessageField1) ; - std::unique_ptr
b(new IMessageField1) ; - std::unique_ptr
c(new IMessageField1) ; - std::unique_ptr
d(new IMessageField1) ; - std::unique_ptr
e(new IMessageField1) ; - addMessageField({ a.get(), b.get(), c.get(), d.get(), e.get() });
- }
上面代码1和2的方式都可以实现功能,2的方式实际上也是先临时生成一个std::initializer_list,再调用std::vector的构造函数临时生成一个std::vector,最后再用刚生成的std::vector初始化pTest,相比1的方式,多了几重复制,效率比较低,一般采用1的方式实现功能。
示例2:
- #include
- #include
- #include
-
- template <class T>
- struct S {
- std::vector
v; - S(std::initializer_list
l) : v(l) { - std::cout << "constructed with a " << l.size() << "-element list\n";
- }
- void append(std::initializer_list
l) { - v.insert(v.end(), l.begin(), l.end());
- }
- std::pair<const T*, std::size_t> c_arr() const {
- return {&v[0], v.size()}; // 在 return 语句中复制列表初始化
- // 这不使用 std::initializer_list
- }
- };
-
- template <typename T>
- void templated_fn(T) {}
-
- int main()
- {
- int a1[] = { 1,2,3,4,5,6 }; //数组拷贝初始化
- int a2[]{ 5,6,7,8,9,0 }; //数组直接初始化
-
- S<int> s = {1, 2, 3, 4, 5}; // 复制初始化
- s.append({6, 7, 8}); // 函数调用中的列表初始化
-
- std::cout << "The vector size is now " << s.c_arr().second << " ints:\n";
-
- for (auto n : s.v)
- std::cout << n << ' ';
- std::cout << '\n';
-
- std::cout << "Range-for over brace-init-list: \n";
-
- for (int x : {-1, -2, -3}) // auto 的规则令此带范围 for 工作
- std::cout << x << ' ';
- std::cout << '\n';
-
- auto al = {10, 11, 12}; // auto 的特殊规则
-
- std::cout << "The list bound to auto has size() = " << al.size() << '\n';
-
- // templated_fn({1, 2, 3}); // 编译错误!“ {1, 2, 3} ”不是表达式,
- // 它无类型,故 T 无法推导
- templated_fn
int>>({1, 2, 3}); // OK - templated_fn
int>>({1, 2, 3}); // 也 OK - }
输出:
- constructed with a 5-element list
- The vector size is now 8 ints:
- 1 2 3 4 5 6 7 8
- Range-for over brace-init-list:
- -1 -2 -3
- The list bound to auto has size() = 3
示例3:
- struct MyTest{
- explicit X(int a, int b) :a(a), b(b) { std::cout << "MyTest(int a,int b)\n"; }
-
- int a{};
- int b{};
- };
-
- int main() {
- MyTest x{ 1,2 }; //OK
- MyTest x2( 1,2 ); //OK
- MyTest x3 = { 1,2 }; //Error
- }
MyTest x3 ={1,2};
参考复制初始化的规则:复制列表初始化(考虑 explicit 和非 explicit 构造函数,但只能调用非 explicit 构造函数)
3.C++之可变参数模版
3.1.简介
一个可变参数模板就是一个可以接受可变参数的模版函数或模板类;参数的类型是一种模板,是可经推导的,可以是任意存在的类型(系统类型或自定义类型);参数数目可变的,可以包括零个、一个或多个;可变数目的参数被称为参数包(parameter packet)。存在两种参数包:模板参数包(template parameter packet),表示零个或多个模板参数;函数参数包function parameterpacket),表示零个或多个函数参数。如:
- template<typename... Arguments> class vtclass;
-
- vtclass< > vtinstance1;
- vtclass<int> vtinstance2;
- vtclass<float, bool> vtinstance3;
- vtclass<long, std::vector<int>, std::string> vtinstance4;
3.2.可变参数个数
利用sizeof...()计算可变参数的大小,如:
- template<class... Types>
- struct count
- {
- static const std::size_t value = sizeof...(Types);
- };
3.3.递归包展开
C++的包展开是通过 args...
的形式,后面...
就意味着展开包,需要两个函数:递归终止函数 和 递归函数,过程就是参数包在展开的过程中递归调用自己,每调用一次参数包中的参数就会少一个,直到所有的参数都展开为止,当没有参数时,则调用递归终止函数终止递归过程。如下:
- #include
-
- using namespace std;
-
- void print() {
- cout << endl;
- }
-
- template <typename T>
- void print(const T& t) { //边界条件
- cout << t << endl;
- }
-
- template <typename First, typename... Rest>
- void print(const First& first, const Rest&... rest) {
- cout << first << ", ";
- print(rest...); //打印剩余参数,注意省略号必须有
- }
-
- int main()
- {
- print(); // calls first overload, outputting only a newline
- print(1); // calls second overload
-
- // these call the third overload, the variadic template,
- // which uses recursion as needed.
- print(10, 20); //输出: 10, 20
- print(100, 200, 300); //输出:100, 200, 300
- print("first", 2, "third", 3.14159); //输出: first, 2, third, 3.14159
- }
3.4.逗号表达式展开
逗号表达式是会从左到右依次计算各个表达式,并将最后一个表达式的值作为返回值返回;我们将最后一个表达式设为整型值,所以最后返回的是一个整型;将处理参数个数的动作封装成一个函数,将该函数作为逗号表达式的第一个表达式;…代表参数包,列表展开。如:
- template <class T>
- void printArg(T t) {
- cout << t << endl;
- }
-
- //展开参数包
- template <class ...Args>
- void expand(Args... args) {
- int arr[] = { (printArg(args), 0)... };
- }
- int main()
- {
- expand(1);
- expand(1, 'A');
- expand(1, "hello", 3);
- return 0;
- }
函数执行expand(1, "hello", 3);的时候,调用expand,数组arr初始化会展开args参数,变化为:
int arr[] = {(printArg(1), 0), (printArg("hello"), 0), (printArg(3), 0)};
根据逗号表达式的规则,arr[] 还是 {0,0,0};
另外,还可以利用std::initializer_list 可以接收任意长度的初始化列表来展开包,如:
- template<class F, class... Args>
- void expand(const F& f, Args&&...args) {
- std::initializer_list<int>{(f(std::forward< Args>(args)), 0)...};
- }
-
- int main()
- {
- expand([](int i) { cout << i << endl; }, 23, 44, 2423);
- return 0;
- }
3.5.Lambda 捕获
包展开可以在 lambda 表达式的捕获子句中出现:
- template<class... Args>
- void f(Args... args)
- {
- auto lm = [&, args...] { return g(args...); };
- lm();
- }
3.6.转发参数包
在C++11标准下,我们可以组合使用可变参数模板与std::forword机制来编写函数,实现将其实参不变地传递给其他函数,关于std::forward的详解讲解,可参考我的博客:C++之std::forward_c++ std::forward-CSDN博客
借助std::forward
就可以实现参数的完美转发了,如STL中map的插入函数emplace下:
- template <class... _Valty>
- iterator emplace(_Valty&&... _Val)
- {
- return _Mybase::emplace(_STD forward<_Valty>(_Val)...).first;
- }
4.总结
纸上得来终觉浅,绝知此事要躬行。
参考
评论记录:
回复评论: