目录

C++变参模板运用实战——实现PrintLn


想要实现PrintLn,关键在于支持无限个参数的打印函数,所以我大致总结下C++能够如何去实现它!

方式一:用初始化列表实现PrintLn() 【C++11】

版本一:朴素初始化列表版本版本

函数版本:

#include<iostream>
using namespace std;

void PrintLn(std::initializer_list<int> args){
    for(auto arg:args){
        cout<<arg<<", ";
    }
    cout<<std::endl;
}
int main() {
    PrintLn({3,23,2,12});
    return 0;
}

类的构造器版本(可去掉小括号):

#include<iostream>
using namespace std;

class PrintLn {
public:
    PrintLn(std::initializer_list<int> args) {
        for (auto arg:args) {
            cout << arg << ", ";
        }
        cout << std::endl;
    }
};

int main() {
    auto t = PrintLn{3, 23, 2, 12};
    return 0;
}

版本二:加模板参数版本

实际上和上面的基本没太大区别,除了上面确定为了int类型,二加了模板后,可以为任意类型,但是实际上传入的还是只能是同一个类型。所以初始化列表实现是非常的不好用的。

#incldue<iostream>
using namespace std;

template<typename T>
class PrintLn {
public:
    PrintLn(std::initializer_list<T> args) {
        for (auto arg:args) {
            cout << arg << ", ";
        }# PrintLn函数实现

想要实现PrintLn,关键在于支持无限个参数的打印函数,所以我大致总结下C++能够如何去实现它!

## 方式一:用初始化列表实现PrintLn() 【C++11】

### 版本一:朴素初始化列表版本版本

> 函数版本:

```cpp
#include<iostream>
using namespace std;

void PrintLn(std::initializer_list<int> args){
    for(auto arg:args){
        cout<<arg<<", ";
    }
    cout<<std::endl;
}
int main() {
    PrintLn({3,23,2,12});
    return 0;
}

类的构造器版本(可去掉小括号):

#include<iostream>
using namespace std;

class PrintLn {
public:
    PrintLn(std::initializer_list<int> args) {
        for (auto arg:args) {
            cout << arg << ", ";
        }
        cout << std::endl;
    }
};

int main() {
    auto t = PrintLn{3, 23, 2, 12};
    return 0;
}

版本二:加模板参数版本

实际上和上面的基本没太大区别,除了上面确定为了int类型,而加了模板后,可以为任意类型,但是实际上传入的还是只能是同一个类型。所以初始化列表的方式实现Println只能说形似而神不似。

#incldue<iostream>
using namespace std;

template<typename T>
class PrintLn {
public:
    PrintLn(std::initializer_list<T> args) {
        for (auto arg:args) {
            cout << arg << ", ";
        }
        cout << std::endl;
    }
};

int main() {
    auto t = PrintLn<int>{3, 23, 2, 12};
    return 0;
}

方式二:用可变参模板实现 【C++11/17】

如果有了解过C的可变参函数和可变参的宏,那么这个可变参模板与它有些类型,只不过C里面的va_start,va_list,va_arg,va_end这一系列实现可变参数的宏用起来非常麻烦,而且无法确定每个参数的类型,而可变参的模板则带有模板的泛型性质,所以是能确定类型的,甚至由于模板可以传值,后面还可直接传值使用。

以下简单描述可变参模板的使用方式:

  1. typenam... 算C++的一个新的关键字,它可以用来定义一个可变参的模板类型,而这个类型在其他地方定义使用的时候也要在后面带上 ... 表示拆包,否则会报错。 例如:

    template<typename... T>
    void f(T... t){//TODO 这种类型或变量在任何地方作为参数定义或者传递的时候都需要加上...表示拆包
        f(t...)
    }
    
  2. 在C++17出现fold expression之前,这个拆包过程只能借助另一个模板参数来得到模板参数包里面的内容。

注意以上两点,那么可以开始编写泛型模板,实现可变参数的完全打印过程了。

C++11版本实现

错误实现版本:如果你直接像下面这样进行拆包,那么编译是会报错的,因为拆包过程相当于一个递归的过程,而你这个递归的过程没有一个跳出的条件,比如args如果为0个参数时,继续在往下就无法展开了,所以需要实现一个没有参数的版本让拆包过程停止。

template<typename T,typename... Args>
void PrintLn(T firstArg,Args... args){
    cout<<firstArg<<", ";
    PrintLn(args...);
}
//拆包过程:PrintLn(3,1,3,4)->
// PrintLn(firstArg:3,args(1,3,4));
// PrintLn(firstArg:1,args(3,4));
// PrintLn(firstArg:3,args(4));
// PrintLn(firstArg:4,args(null))
// 由于到了上面的第四行还要继续往下拆包
// 而此时只有0个参数,没有对应的PrintLn版本可以调用,故报错!

以下为正确修改版本:

#include<iostream>
using namespace std;

void PrintLn(){

}
template<typename T,typename... Args>
void PrintLn(T firstArg,Args... args){
    cout<<firstArg<<", ";
    PrintLn(args...);
}

int main() {
    PrintLn(3, 23, 2, 12);
    return 0;

当然也可以控制只剩一个参数时就停止拆包。

#include<iostream>
using namespace std;

template<typename T>
void PrintLn(T arg){
    cout<<arg<<endl;
}
template<typename T,typename... Args>
void PrintLn(T firstArg,Args... args){
    cout<<firstArg<<", ";
    PrintLn(args...);
}

int main() {
    PrintLn(3, 23, 2, 12);
    return 0;
}

C++17版本实现

上面的实现流程实际上在C++17中可以用 if constexpr()+sizeof… 在编译期间来进行流程控制。

首先来讲一讲为什么普通的 if + sizeof… 来实现可变参数的长度控制流程会报错呢?

因为整个模板推断和拆包解包过程是在编译期完成的,而if的控制流程在编译期是完全不清楚的,所以会报错,但是有了if constexpr之后,就能控制编译期的模板拆包过程了!

如上面实现PrintLn,可以直接简化成下面这样:

#include<iostream>
using namespace std;

template<typename T,typename... Args>
void PrintLn(T firstArg,Args... args){
    if constexpr(sizeof...(args)==0){//当参数个数为0个的时候就不继续拆包了
        cout<<firstArg<<endl;
    }else{
        cout<<firstArg<<", ";
        PrintLn(args...);//往下继续拆包
    }
}

int main() {
    PrintLn(3, 23, 2, 12);
    return 0;
}

方式三:可变参模板的fold expression展开 【C++17】

在C++17中,加入了一个fold expression的语法,让可变参数模板可以不通过递归的方式来解包,直接把每个包解开放入一个表达式,然后剩余的包都以该表达式解开,基本的语法如下:

((expression)op...);

expression : 表示希望每个解开的参数所执行的表达式。 op : 你指定的操作符。 ... : 一直不断的解包,由于此处放的位置是右边,所以往右边解包,如果放左边则往左边解包。

示例代码:

#include <bits/stdc++.h>
using namespace std;

template<typename... Args>
double sum(Args... args){
    return (args+...);//等价于3+23+1+3.32
}
template<auto... val>//可变的传值的模板参数
constexpr int sum(){
    return (val+...);
}

int main() {
    cout<<sum<3,23,1,32>()<<endl;//传值模板参数不支持浮点类型,所以全用的int类型
    cout<<sum(3,23,1,3.32);
    return 0;
}

简单的利用fold expr实现

  • 基于以上对fold expr的使用,我们来正式实现PrintLn,值得一提的是,这个fold expr的性能肯定是比之前递归解包的性能要好的,因为只是迭代的拓宽而已。

我们可以将拆开的包用 ',' 展开

#include<iostream>
using namespace std;

template<typename... Args>
void PrintLn(Args... args){
    ((cout<<args<<", "),...)<<endl;
}

int main() {
    PrintLn(1,2.3,"LB","hhh");
    return 0;
}

加上流程控制实现

通过更复杂的流程控制把最后一个打印出来的逗号去掉。

通过延申三元运算符,使得运行时能够正确的打印最后一次。

反正我这里编译期只负责文本替换,所以被fold expr展开的表达式并不会有什么要是编译期常量的要求。

这一切都看作简单宏替换即可。

#include<iostream>
using namespace std;

template<typename... Args>
void PrintLn(Args... args){
    int lastIndex = sizeof...(args)-1;//得到传入的参数长度
    int i = 0;
    ((i++==lastIndex?cout<<args<<endl:cout<<args<<", "),...);
}

int main() {
    PrintLn(1,2.3,"LB","hhh");
    return 0;
}

更多fold expr运用…

利用与或表达式展开,然后利用它们的短路性质,实现得到拆包元素的精准打击(获得包里的第几个元素)。

#include<iostream>
using namespace std;

template<typename... Args>
auto GetNth(int n, Args... args) {
    int i = 0;
    using CommonType = common_type_t<Args...>;
    CommonType ret;
    ((i++ == n && (ret = args, true))||...);
    return ret;
}

int main() {
    cout << GetNth(3, 2, 1, 2.3, 32.2);
    return 0;
}
  • 上面为了存储不确定的类型用了common_type_t,这个可以帮助你得到一个公共可用的类型,而这个类型必须是公共可用,比如int了float型可以进行相互转化所以有公共类型,而 char* 和int类型则没有,所以这个GetNth中的元素不能传递 char* 类型的同时传递int类型。