C++变参模板

        从c++11开始,模板可以接受一组数量可变的参数,这种技术称为变参模板。

变参模板

        下面一个例子,通过变参模板打印一组数量和类型都不确定的参数。

#include <iostream>
#include <string>

void print(void)
{
    std::cout<<"........................"<<std::endl;
}

template <typename T, typename ... Ts>
void print(T arg1, Ts ... args)
{
    std::cout<<arg1<<std::endl;
    print(args ...);
}

int main(int argc, char **argv)
{
    print("hello", 7.5, 10, std::string("building"));
}

        看到上面这段代码,首先会产生两个疑问:

  • void print(T arg1, Ts ... args)中arg1是什么作用?
  • void print(void)有什么作用?

        下面将通过解释这段代码的运行过程,来解答上面的问题。仔细观察上面这段代码,不难发现,print函数模板是一个递归函数模板。执行过程大体如下:

  1. main函数调用print("hello", 7.5, 10, std::string("building"));,"hello"赋值给arg1,其余参数赋值给args
  2. print函数输出"hello",再次调用自身print(7.5, 10, std::string("building"));,7.5赋值给arg1,其余参数赋值给args
  3. print函数输出7.5,再次调用自身print(10, std::string("building"));,10赋值给arg1,其余参数赋值给args
  4. print函数输出10,再次调用自身print(std::string("building"));,"building"赋值给arg1,args为空
  5. print函数输出"building",因为args为空,此时不在调用自身,而是重载函数print(void),然后结束递归。

        从整个过程来看,arg1的主要作用就是从args迭代取值,print(void)负责处理args为空的情况。那么不定义void print(void)是否可以呢?答案是否定的,不定义该函数,编译将会报错“No matching function for call to 'print'”。

        此处还应该注意一个问题,print和c/c++的printf原理不一样:printf通过va_list实现变参,而print函数模板是为每种情况都生成了一个重载函数,如下:

        上面的信息来自于xcode调试,当然,也可以通过objdump查看,也会得到相同的结果,编译器确实生成了多个print重载函数:

         当然,上面的代码还可以写成下面的样子:

template <typename T>
void print(T arg)
{
    std::cout<<arg<<std::endl;
}

template <typename T, typename ... Ts>
void print(T arg1, Ts ... args)
{
    print(arg1);
    print(args ...);
}

        如果代码中没有print(arg1),程序知会打印最后一个参数building,print只有迭代到最后一个参数时,才会找到合适的函数print(T arg)。

        但一定要注意,下面的实现方式是错误的,无递归结束条件,无限迭代,直到耗尽堆栈空间:

void print(void)
{
}

template <typename ... Ts>
void print(Ts ... args)
{
    print(args ...);
}

折叠表达式

        从c++17开始,c++引入了一种更为简洁灵活的编程方式——折叠表达式,下面是一个简单的例子:

#include <cstdio>

template <typename ...T>
auto sum(T ... args)
{
    return (... + args);
}

int main(int argc, char **argv)
{
    int s = sum(1, 2, 3, 4, 5);
    printf("%d\n", s);
}

        几乎所有的二元运算符都可以用于折叠表达式,下面是一些其他运算符的例子:

template <typename F, typename ...T>
auto apply(F f, T ...args)
{
    return (f(args), ...);
}

template <typename ...T>
bool and_op(T ...args)
{
    return (args && ...);
}

        迭代表达式,仅仅是围绕一个操作符简单地展开,例如连加。因此,对于三元操作符:?,很难用迭代表达式来实现。所以,想使用迭代表达式和:?求一个集合中的极值,是无法实现的。但是可以通过其他方式实现,下面便是一种实现方式:

template <typename T>
struct min_op final
{
public:
    min_op(T data) : is_first(true), min_data(data) {
        
    }
    
    T operator()(T rhs) {
        if (is_first) {
            is_first = false;
            min_data = rhs;
            return min_data;
        }
        
        min_data = min_data < rhs ? min_data : rhs;
        return min_data;
    }
    
private:
    bool is_first;
    T min_data;
    
};

template <typename T, typename ...Ts>
auto min(T arg, Ts ... args)
{
    min_op<T> op(arg);
    return (op(args), ...);
}

        很明显,这种方式还不如直接使用for循环直接利索。

        与之前的递归迭代方式相比,迭代表达式最大的优点是编译器没有为其生成过多的重载函数。迭代表达式与之前的优点:不使用vector,不会生成多个函数,缺点:解决元素较少的情况。

        变参模板的优点:

  • 可以支持不同的类型
  • 可以不使用容器
  • 直接访问元素,效率比较高

        但其并不是完美无缺的:

  • 不适用元素较多的情况
  • 使用递归迭代会生成大量的重载函数

变参类模板和变参表达式

变参表达式

        函数参数包除了转发所有参数外,还可以做其他事,例如计算他们的值。

template <typename ... Ts>
void print_doubled(Ts ... args)
{
    print((args + args) ...);
}

...
print_doubled(1, 2, 3, 4, 5, 6);
...

变参下标

        作为另外一个例子,下面的函数通过一组变参下标来访问第一个参数中相应的元素:

template<typename T, typename ...IDS>
void print_elems(T a, IDS ...ids)
{
    print(a[ids]...);
}

...
std::vector<int> v{1, 2, 3, 4, 5, 6};
print_elems(v, 1, 3, 5);
...

变参模板类

        提到变参模板类,首先会想到std::tuple,该种技术使得不定义新类型的前提下,多值返回成为一种可能,提供了更加灵活的编程方式,例如:

template <typename T>
std::tuple<T, T, T,  T> calc(T x, T y)
{
    return std::make_tuple(x + y, x - y, x * y, x / y);
}

...
auto result = calc(10.0, 2.5);
...

 变参基类

        变参基类从不定数的基类派生出一个新的类,主要目的是代码复用,比普通写法更加方便,派生类无需引入基类头文件,但需要注意多继承陷阱。下面是一个简单的例子:

#include <cstdio>

struct fly_animal
{
    void fly(void) { printf("flying !\n"); }
};

struct swim_animal
{
    void swim(void) { printf("swiming !\n"); }
};

struct run_animal
{
    void run(void) { printf("running !\n"); }
};

struct fish 
{
    //...
};

struct bird 
{
    //...
};

struct mammal
{
    //....
};


template <typename ...Bases>
struct overloader : Bases...
{
    //using Bases::operator()...;
};


int main(int argc, const char **argv)
{
    using flyfish = overloader<fly_animal, swim_animal>;
    flyfish ff;
    ff.fly();
    ff.swim();
    
    using crocodile = overloader<run_animal, swim_animal>;
    crocodile ccdl;
    ccdl.run();
    ccdl.swim();
    
    using cat = overloader<mammal, run_animal>;
    cat ct;
    ct.run();
    
    return 0;
}

  • 7
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值