C中的可变参数

一切都从函数传参开始说起。我们知道,在C语言中有个神奇的函数:

  1. printf("%s : %d\n","gemfieldnumber",7030);

这个函数可以传递可变参数,说到“可变”参数,主要是指两点可变:1,参数数量可变;2,参数类型可变。比如上面演示的C库中的printf,数量是可变的,类型也是可变的。借助C语言提供的va_list、va_start、va_arg、va_end宏,可以轻松实现类似的可变参数。

  1. va_arg:宏定义,用来获取下一个参数
  2. va_start:宏定义,开始使用可变参数列表,用最后一个具有参数的类型的参数去初始化 va_list
  3. va_end:宏定义,结束使用可变参数列表
  4. va_list:类型,存储可变参数的信息
  1. #include <cstdarg>
  2. void syszuxPrint(int n, ...){
  3. va_list args;
  4. va_start(args, n); // 最后一个具有参数的类型的参数去初始化 va_list
  5. while(n--){
  6. std::cout<<va_arg(args, int)<<", ";
  7. }
  8. va_end(args);
  9. std::cout<<std::endl;
  10. }
  11. int main(int argc, char** argv)
  12. {
  13. syszuxPrint(3, 719,7030,27030);
  14. }

这里只是实现了参数的可变,参数类型如何可变呢?其实也可以,主要到va_arg宏的第二个参数 int 了吗?这种硬编码限制了目前我们只能传递int类型。

  1. while(n--){
  2. if(n == 0){
  3. std::cout<<va_arg(args, const char*)<<", ";
  4. continue;
  5. }
  6. std::cout<<va_arg(args, int)<<", ";
  7. }
  8. syszuxPrint(3, 719,7030,"civilnet");

printf 这样的函数,为什么第一个参数总是“格式化字符串”,printf(“%s : %d\n”,”gemfield number”,7030); 中的”%s : %d\n”?因为:格式化字符串告诉了两点关键信息:可变参数的个数(百分号的个数)、可变参数的类型(%s、%d等)。而C++的可变参数模板克服了这些限制。

可变参数模板的基础原理

C++的可变参数模板是怎么做到不需要告诉参数个数,也不需要告诉参数类型的呢?这仰仗于C++以下的功能:

  1. 函数重载,依靠参数的pattern去匹配对应的函数;
  2. 函数模板,依靠调用时传递的参数自动推导出模板参数的类型;
  3. 类模板,基于partial specialization来选择不同的实现;
  1. void syszuxPrint(){std::cout<<std::endl;}
  2. template<typename T, typename... Ts>
  3. void syszuxPrint(T arg1, Ts... arg_left){
  4. std::cout<<arg1<<", ";
  5. syszuxPrint(arg_left...);
  6. }
  7. int main(int argc, char** argv)
  8. {
  9. syszuxPrint(719,7030,"civilnet");
  10. }

在上述代码中,main函数里调用了syszuxPrint(719,7030,”civilnet”); 会导致 syszuxPrint 函数模板首先展开为:
void syszuxPrint(int, int, const char*)

在打印第1个参数719后,syszuxPrint递归调用了自己,传递的参数为 arg_left…,该参数会展开为【7030,”civilnet”】,syszuxPrint第2次进行了展开:void syszuxPrint(int, const char*)

在打印第1个参数7030后,syszuxPrint递归调用了自己,传递的参数为 arg_left…,该参数会展开为【”civilnet”】,syszuxPrint第3次进行了展开:void syszuxPrint(const char*)

在打印第1个参数”civilnet”后,syszuxPrint递归调用了自己,传递的参数为arg_left…,该参数会展开为【】,syszuxPrint准备进行第4次展开:void syszuxPrint(),但是,我们已经定义了这个函数:

void syszuxPrint(){std::cout<<std::endl;}上面这个函数是函数模板syszuxPrint的“非模板重载”版本,于是展开停止,直接调用这个“非模板重载”版本,递归停止。syszuxPrint的“非模板重载”版本,目的就是为了递归能够最终退出。

sizeof…

上面定义的syszuxPrint可变参数模板比起C语言的VA_*来说,不知道是好到哪里去了,但是还有一点让人不舒服:它总是需要定义2次,目的只是为了让递归退出。有没有更优雅的办法呢?

C++11引入了sizeof…操作符,可以得到可变参数的个数(注意sizeof…的参数只能是parameter pack,不能是其它类型的参数啊),如下所示:

  1. std::cout<<"DEBUG: "<<sizeof...(Ts)<<" | "<<sizeof...(arg_left)<<std::endl;

这样可以打印出parameter的个数。你一定这样想了,如果通过sizeof…判断出arg_left这个parameter pack的个数为零了,那就退出递归调用不好吗?就像下面这样:

  1. template<typename T, typename... Ts>
  2. void syszuxPrint(T arg1, Ts... arg_left){
  3. std::cout<<arg1<<", ";
  4. if(sizeof...(arg_left) > 0){
  5. syszuxPrint(arg_left...);
  6. }
  7. }
  8. int main(int argc, char** argv)
  9. {
  10. syszuxPrint(719,7030,"civilnet");
  11. }

你看,syszuxPrint只定义了1次,我们在函数体里使用了sizeof…,如果parameter pack为零了,我们就停止递归调用。但不幸的是,编译程序报错:

  1. civilnet.cpp: In instantiation of void syszuxPrint(T, Ts ...) [with T = const char*; Ts = {}]’:
  2. civilnet.cpp:211:20: recursively required from void syszuxPrint(T, Ts ...) [with T = int; Ts = {const char*}]’
  3. civilnet.cpp:211:20: required from void syszuxPrint(T, Ts ...) [with T = int; Ts = {int, const char*}]’
  4. civilnet.cpp:217:36: required from here
  5. civilnet.cpp:211:20: error: no matching function for call to syszuxPrint()’
  6. 211 | syszuxPrint(arg_left...);
  7. | ~~~~~~~~~~~^~~~~~~~~~~~~
  8. civilnet.cpp:208:6: note: candidate: template<class T, class ... Ts> void syszuxPrint(T, Ts ...)’
  9. 208 | void syszuxPrint(T arg1, Ts... arg_left){
  10. | ^~~~~~~~~~~
  11. civilnet.cpp:208:6: note: template argument deduction/substitution failed:
  12. civilnet.cpp:211:20: note: candidate expects at least 1 argument, 0 provided
  13. 211 | syszuxPrint(arg_left...);
  14. |

核心错误是这句【civilnet.cpp:211:20:error: no matching function for call to ‘syszuxPrint()’】,啥意思啊?为什么还在试图调用空的syszuxPrint?为啥sizeof…那个if条件表达式没有生效?

这是因为,可变参数模板syszuxPrint的所有分支都被instandiated了,并不会考虑上面那个if表达式。一个instantiated 的代码是否有用是在 runtime 时决定的,而所有的 instantiation 是在编译时决定的。所以syszuxPrint() 空参数版本照样被 instandiated,而当instandiated的时候并没有发现对应的实现,于是编译期报错。

C++17的if constexpr表达式和梦想实现

C++17中引入了编译期if表达式(if constexpr),可以用来完美的解决这个问题:

  1. template<typename T, typename... Ts>
  2. void syszuxPrint(T arg1, Ts... arg_left){
  3. std::cout<<arg1<<", ";
  4. if constexpr(sizeof...(arg_left) > 0){
  5. syszuxPrint(arg_left...);
  6. }
  7. }
  8. int main(int argc, char** argv)
  9. {
  10. syszuxPrint(719,7030,"civilnet");
  11. }

上面的代码中,我们使用了if constexpr,完美编译成功:g++ -std=c++17 civilnet.cpp -o civilnet。注意-std=c++17,g++还没有默认启用C++17的feature。

Fold Expressions

image.png
比如求parameter pack的和:

  1. template<typename... T>
  2. auto foldSum (T... s) {
  3. return (... + s); // ((s1 + s2) + s3) ...
  4. }

再比如上面的print例子可以简写成:

  1. template<typename... Types>
  2. void print (Types const&... args) {
  3. (std::cout << ... << args) << '\n';
  4. }

但是上面这样写中间就没有了间隔符,我们可以实现一个帮助类,利用类中的重载操作符来达到对应的效果。

https://stackoverflow.com/questions/27582862/fold-expressions-with-arbitrary-callable

  1. template<typename T>
  2. class AddNewLine {
  3. private:
  4. const T& ref; // 存放print函数调用的参数
  5. public:
  6. AddNewLine(const T &r): ref(r) {}
  7. friend std::ostream& operator<<(std::ostream& os, AddNewLine<T> x) {
  8. os << x.ref << std::endl;
  9. return os;
  10. }
  11. };
  12. template<typename ...Args>
  13. void print(Args ...args) {
  14. (std::cout << ... << AddNewLine<Args>(args)); // wapper 类初始化接受两个pack
  15. }
  16. int main(int argc, char** argv)
  17. {
  18. print(12,123,41,5123);
  19. }

(init op pack1) op pack2 ,显然此时 pack 是 AddNewLine(args))

可变参数表达式

  1. template<typename... T>
  2. auto foldSum(const T&... s){
  3. syszuxPrint(s + s...);
  4. }
  5. int main(int argc, char** argv)
  6. {
  7. foldSum(719,7030, std::string("CivilNet"));
  8. }

注意看其中的syszuxPrint(s + s…)用法,真是让人瞠目结舌啊。这个表达式就相当于syszuxPrint( (719 + 719), (7030+7030), (string(“CivilNet)+string(“CivilNet)) )。同理:syszuxPrint(1 + s…)相当于将参数展开后的每个参数加1。

Variadic Base Blasses(可变参数基类)

  1. class Gemfield
  2. {
  3. public:
  4. void test1(){
  5. std::cout<<"This is base class Gemfield."<<std::endl;
  6. }
  7. };
  8. class CivilNet
  9. {
  10. public:
  11. void test2(){
  12. std::cout<<"This is base class CivilNet."<<std::endl;
  13. }
  14. };
  15. template<typename... Bases>
  16. class SYSZUX : public Bases...{};
  17. int main(int argc, char** argv)
  18. {
  19. SYSZUX<Gemfield, CivilNet> syszux;
  20. syszux.test1();
  21. syszux.test2();
  22. }

根据模板参数去选择性地继承