C++可变形参实现

可变形参是指调用函数时参数的数量和类型可能发生变化。有三种方式定义函数的可变形参,分别是initializer_list可变参数模板省略符

可变形参的原理:C/C++函数的参数是存放在栈区的,并且参数的入栈是从参数的右侧开始的,即最后一个参数先入栈,而第一个参数最后才入栈。所以,根据栈先入后出的性质,函数总是能够先找到第一个参数,然后依次找到后面的参数。

1 initializer_list

如果函数的实参数量未知但是全部实参的类型相同,可以使用initializer_list类型的形参。

initializer_list是标准库类型,定义在<initializer_list>头文件中,用于表示某种特定类型的值的数组

支持的操作:

操作含义
initializer_list lst默认初始化,T类型元素的空列表
initializer_list lst{a,b,c,…}lst的元素和初始值一样多,lst的元素是对应初始值的副本,列表中的元素是const
lst2(lst) 或 lst2 = lst拷贝或赋值一个initializer_list对象,不会拷贝列表中的元素,拷贝后,原始列表和副本共享元素
lst.size()返回列表中的元素数量
lst.begin()返回lst中首元素的地址
lst.end()返回lst中尾元素下一位置的指针

initializer_list也是一种模板类型,定义initializer_list对象时,必须指明所包含的元素的类型,如:

initializer_list<std::string> ls;
initializer_list<int> li;

initializer_list中包含的是常量,不能对其包含的元素进行修改。如:
在这里插入图片描述可以通过initializer_listbeginend成员对其进行遍历。如:

void error_msg(std::initializer_list<std::string> msg)
{
	for (auto iter = msg.begin(); iter != msg.end(); ++iter)
	{
		std::cout << *iter << std::endl;
	}
}

调用方式是:

error_msg({ "this","is a","sentence" });

不要省略调用时的{},否则传入的就是三个字符串,而不是一个std::initializer_list<std::string>对象。

使用initializer_list形参时,还可以同时使用其他类型的形参,前后顺序没有规定,如:

void error_msg(std::initializer_list<std::string> msg,int n)
{
	for (auto iter = msg.begin(); iter != msg.end(); ++iter)
	{
		std::cout << *iter << std::endl;
	}

	std::cout << n << std::endl;
}

//调用方式:
error_msg({ "this","is a","sentence" },3);

2 可变参数模板

2.1 定义

一个可变参数模板就是一个接受可变数目参数的模板函数或者模板类。其接受的参数类型和数目都可变

可变数目的参数被称为参数包,存在两种参数包:模板参数包,表示零个或多个模板参数;函数参数包,表示零个或多个函数参数。

用一个省略号来指出一个模板参数或函数参数表示一个包。在一个模板参数列表中,class...typename...指出接下来的参数表示零个或多个类型的列表。一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。如:

//Args是一个模板参数包,表示零个或多个额外的类型参数
//rest是一个函数参数包,表示零个或多个函数参数
template <typename T,typename... Args>
void foo(const T &t,const Args& .. rest);

编译器从函数的实参推断模板参数类型,对于一个可变参数模板,编译器还会推断包中参数的数目

如对于下面的调用:

foo(42,"hi",2.0,true);//编译器推断包中有三个参数,分别是const char*、double、bool类型
foo("yes",42,"hi");//编译器推断包中有两个参数,分别是int、const char*类型

2.2 sizeof…运算符

sizeof...运算符用于计算包中的参数数量。返回的是一个常量表达式,且不会对其实参求值

template <typename... Args>
void g(Args... args)
{
	std::cout << sizeof...(Args) << std::endl;//计算类型参数的数目
	std::cout << sizeof...(args) << std::endl;//计算函数参数的数目
}

2.3 可变参数函数的调用

可变参数函数是递归调用的,第一步处理包中的第一个实参,然后用剩余实参调用自身。为了终止递归,还需实现一个处理最后一个实参的函数。如按照下列方式定义print函数:

template <typename T>
void print(std::ostream& os,const T& val)
{
	os << val << std::endl;//打印函数包中最后一个实参
}

template <typename T,typename... Args>
void print(std::ostream& os,const T& val,const Args&... args)
{
	os << val << " ,";//打印当前第一个实参
	print(os,args...);//递归调用,打印其他实参
}

如按照print(std::cout,42,"hi",true,3.0);进行函数调用,那么编译器会实例化出一个void print(std::ostream&,const int&,const char[3]&,const bool&,const double&)函数。首先调用下面的print函数,第一个参数推断为int,参数包中的参数类型为const char*,const bool,const double,在函数中首先打印出第一个实参,然后调用自身,调用时参数包中的内容为"hi",true,3.0;然后递归调用自身,直到函数包中只有一个参数时,即需要执行print(os,3.0)时,此时既可以调用自身,也可以调用上面的print函数,调用自身时,认为参数包args是空包。但编译器更倾向于使用上面版本的print函数,因为非可变参数版本比可变参数模板更加特例化

2.4 可变参数函数模板放在哪里?

本节要讨论的核心意思是:可变参数函数模板实现时,应该把声明和实现都放在头文件中?还是应该把声明放在头文件中,实现放在源文件中 ?

答案是:定义和实现都放在头文件中肯定可行;如果声明放在头文件中,实现放在源文件中,据需要在源文件中针对需要进行特例化的函数声明。

如:
Utils.h中定义:

//Utils.h
template <typename T>
void print(std::ostream& os,const T& val)
{
    os << val << std::endl;
}

template <typename T,typename... Args>
void print(std::ostream& os, const T& val,const Args&... rest)
{
    os << val << ", ";
    print(os,rest...);
}

main.cpp中按照如下方式调用,代码是可以正常使用的。

#iinclude <iostream>
#include "Utils.h"
int main()
{
	print(std::cout,1,2.0,"hi",true);
	return 0;
}

如果将可变参数函数模板的声明和实现分开放到头文件和源文件中,还按照原有方式进行调用,则会编译出错:

//Utils.h
#include <iostream>

template <typename T>
void print(std::ostream& os,const T& val);

template <typename T,typename... Args>
void print(std::ostream& os, const T& val,const Args&... rest);

//Utils.cpp
#include "Utils.h"

template <typename T>
void print(std::ostream& os,const T& val)
{
    os << val << std::endl;
}

template <typename T,typename... Args>
void print(std::ostream& os,const T& val,const Args&... rest)
{
    os << val << ", ";
    print(os,rest...);
}

具体的报错信息为:

对‘void print<int, double, bool, double>(std::ostream&, int const&, double const, bool const, double const)’未定义的引用

对函数未定义的引用,表示编译器找不到函数的实现,为什么会出现这个错误?

原因是,C++规定,当一个模板不被用到的时候是不会进行实例化的,在Utils.cpp文件中,并没有用到任何具体的print函数,因此并没有进行实例化实现,也就造成了链接错误。解决办法,如https://blog.csdn.net/bendanban/article/details/51321899所指出的,在Utils.cpp中针对我们需要的版本进行模板函数的实例化,即在Utils.cpp上面添加一行代码:

#include "Utils.h"

//非初始化调用的模板函数无需实例化
/*template void print(std::ostream&,const double&);
template void print(std::ostream&,const bool&,const double&);
template void print(std::ostream&,const double&,const bool&,const double&);*/

//新添加的模板函数实例化代码
template void print(std::ostream&,const int&,const double&,const bool&,const double&);

template <typename T>
void print(std::ostream& os,const T& val)
{
    os << val << std::endl;
}

template <typename T,typename... Args>
void print(std::ostream& os,const T& val,const Args&... rest)
{
    os << val << ", ";
    print(os,rest...);
}

总结下:

可变参数函数模板声明和实现可以同时放在头文件中
也可以将函数声明和实现分别放入头文件和源文件,但这种方式需要在源文件中对模板函数的初始调用形式进行实例化

2.5 包扩展

包扩展就是指将操作或函数应用于包中的每个元素

通过在模式右边放一个省略号可以触发扩展操作。如在const Args&...const Arg&应用到Args中的每个元素。

假设debug_rep为一个函数,具有单个形参。const Args&... rest定义了一个模板参数包,rest中的每个参数都应用debug_rep函数的正确调用方式为:debug_rep(rest)...,注意...应该写在括号外,表示debug_rep(rest1),debug_rep(rest2),...,debug_rep(restn)rest1、rest2、...、restn表示包中的各个参数。如果将...写在括号内,则包扩展之后的调用形式为debug_rep(rest1,rest2,...,restn)是无法正常调用的。

有一个注意的点:对函数应用包扩展时,省略号...不能直接后面跟分号。如下面代码所示:

#include <iostream>

template <typename T>
void print(std::ostream& os,const T& val)
{
    os << val << std::endl;
}

template <typename T,typename... Args>
void print(std::ostream& os,const T& val,const Args&... rest)
{
    os << val << ", ";
    print(os,rest...);
}

template <typename... Args>
void top_print(const Args&... args)
{
    print(std::cout,args)...;
}

int main()
{
    int a = 1;
    top_print(a,2.0,true,1e-4);
    return 0;
}

代码编译会出错,具体为对print(std::cout,args)...;行,报下列错误信息:

error: expected ‘;’ before ‘...’ token
error: parameter packs not expanded with ‘...’

修改的方法有两种,如下所示:

//方法1
template <typename... Args>
void top_print(const Args&... args)
{
    std::initializer_list<int> {(print(std::cout,args),0)...};
}
//方法二
template <typename T,typename... Args>
T equal(const T& val,const Args&... rest)
{
    return val;
}

template <typename... Args>
void top_print(const Args&... args)
{
    print(std::cout,equal(args)...);
}

方法一,将函数包扩展放入{}中,初始化一个initializer_list对象,但注意创建的是一个未命名的临时initializer_list对象,也就是说initializer_list对象不关键,关键的是借助其初始化{}可以进行函数包扩展。在该方法中,还有一个要注意的点,这里使用的print函数是:

template <typename T>
void print(std::ostream& os,const T& val)

因为这个函数无返回值,所以要使用std::initializer_list<int> {(print(std::cout,args),0)...};,即每次调用print函数后都向initializer_list中新加一个0;当然如果具体使用的函数是有返回值的且类型为int,那么就可以更加简化调用方式,写成std::initializer_list<int> {print(std::cout,args)...};形式

总之原则就是创建一个临时的initializer_list对象,循环调用要执行的函数,函数有返回值,initializer_list对象中放返回值;函数无返回值,创建一个临时的返回值。注意返回值/自定义初始化的值要和initializer_list对象中的元素类型匹配

方法二,是加了一个无意义的函数equal,返回各个参数值。目的也是为了把top_print中对于print(std::cout,args)...的调用放入括号中。

两种方式都能正常的执行,但从代码简洁角度出发,推荐方法一。

至于为什么省略号后不能跟分号,必须将函数包扩展放入括号或分号中,个人还没有找到原因,欢迎大牛指导。

具体原因为:Essentially, expanding a parameter pack E... produces a list E1, E2, [...], EN, one E for each element in the pack. This syntactic construct is only valid in places where lists are grammatically correct, such as in function calls, initializer lists etc.来自于https://stackoverflow.com/questions/33868486/parameter-packs-not-expanded-with

3 省略符形参

省略符...形参是为了便于C++程序访问某些特殊的C代码而设置的,使用了名为varargs的C标准库功能。

省略符形参只出现在形参列表中的最后一个位置,只有两种形式:

void foo(param_list,...);
void foo(...);

第一种形式中,param_list形参对应的实参会进行类型检查,省略符所对应的形参无须类型检查param_list后跟的逗号是可选的,可要可不要

C语言的标准头文件stdarg.h提供了一套对可变参函数的实现机制,所以实现可变参函数需要包含该头文件,对应到C++下就是cstdarg头文件。

具体实现上,是依赖一个va_list的数据类型和va_startva_argva_end三个宏进行实现的。

  • va_list实际类型为char*,用于存储所有的参数;
  • va_start用于初始化定义的va_list对象,需要两个参数,第一个为刚才的va_list对象,第二个参数为可变形参的总个数;
  • va_arg用于返回可变参数,需要两个参数,第一个为刚才的va_list对象,第二个为要转换为的参数类型;
  • va_end用于释放va_list对象。

具体实现可变整型数的加法:

#include <cstdarg>
int sum(int n,...)
{
    va_list arg_ptr;
    int nRes = 0;
    va_start(arg_ptr,n);
    for(int i=0;i<n;++i)
    {
        nRes += va_arg(arg_ptr,int);
    }
    va_end(arg_ptr);
    return nRes;
}

int main()
{
	int a = sum(3,15,16,45,100);
    std::cout << a;
    return 0;
}

结果为76而不是176,是因为sum第一个实参为3,只进行前三个数字的求和。

如果按照下面的代码进行调用:

int main()
{
	int a = sum(3,15,16.0,45,100);
    std::cout << a;
    return 0;
}

结果为160,16.0无法转换为整数,所以不参与求和运算。

如果按照下面的代码进行调用:

int main()
{
	int a = sum(4,15,16.0,45,100);
    std::cout << a;
    return 0;
}

结果为-136492192,这是因为现有实参中只有三个可用,现在要读取四个整型数,会读取一个未定义的值,所以返回的结果也是未定义的。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值