• 跟我学c++中级篇——c++11中的模板变参化


    一、变参模板(Variadic Template)

    变参,顾名思意,就是变量是可以动态变化的。变参模板,那么模板中的类型也是可以变化的。而模板又有类模板和函数模板,所以就知道,函数模板和类模板中的参数类型是可以变化的。这有什么用处呢?在设计和开发过程中,经常遇到一些无法确定参数的情况,这时候儿,最简单的方法就是,有一个参数不同,就写一个函数,然后随着时间的流逝就会发现,同样的函数会写很多,只是参数类型或者数量不同罢了。不要以为这是开玩笑,在微软的早期的库里,大量这样的函数,有的函数参数甚至到十来个。那么此时,就可以使用变参函数(模板)来实现。同样,类也是如此。
    但是不是这种情况一一定要这么做,就得看实际的情况中综合考虑了。变参函数最有名的就是C语言中的printf,它的实现机制在前面分析过,有兴趣的可以翻一下它的三种实现机制。另外一个需要说明的是从c++11起,模板中提供了默认参数(template<typename T1, typename T2 = int> class A;),这个要和非类型模板参数区分开来。

    二、变参函数模板

    还是先从变参函数模板开始,先看一个具体的样子有一个形象化的认知:

    template 
    void TestVarTemplate(T... args)
    {
             std::cout<< "args len is:"<< sizeof...(args) << std::endl;
    }
    template
    void TestVarTemplate(T t, Args... args)
    {
    std::cout<< "t is:"<
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    看到那三个点没,这说明这里的模板参数类型是可以动态增加的,上面的这个例子,代表数量可以是0~N个,如果想避免出现0个的情况,就看第二个函数例子。变参函数模板有以下几种解析方式:
    1、递归展开
    我们仍然用前面的例子:

    #include 
    int TestVT()
    {
        return 1;
    }
    //这个函数注释后会有什么效果?对比一下返回值和终止的位置
    template
    int TestVT(T t)
    {
        return  t;
    }
    template
    int TestVT(T var0, Args... varn)
    { 
        std::cout << "args len is:" << sizeof...(varn) << std::endl;
        return var0 + TestVT(varn...);
    }
    int main()
    {
        std::cout << "value is:"<< TestVT(1, 2, 3)<< std::endl;
        system("pause");
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在代码中提出了问题,结果是,如果注释掉模板函数,那么调用一直会延伸到普通函数TestVT,多展开一次。而如果有这个模板函数,则调用到此模板函数即终止递归。如果不太明白,一个是要看一下递归是什么,另外一个明白递归后,下断点调试一下代码,立刻就清楚了。这也是后面提到的递归结束的标志控制方式。

    2、打包推导(逗号表达式)展开
    打包展开的代码示例:

    template < typename T>
    int Add(T arg)
    {
        return    arg;
    }
    
    template < typename... Args>
    int AddValue(Args ... args)
    {
        //也可使用类似int v[] = { (Add(args),0)... };这种逗号表达式代码,只是强制存储0到V数组中
        int v[] = { Add(args)... };
        int d = 0;
    
        for (auto n : v)
        {
            d += n;
        }
    
        return    d;
    }
    
    int TestAdd()
    {
        std::cout << "-----"<< AddValue(3, 3, 3) << std::endl;;
        return 0;
    }
    int main()
    {
        TestAdd();
        system("pause");
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    其实打包展开类似于一种循环展开调用函数或者递推进行。结果是一样的,可以和上面的递归方式对比。

    3、c++17折叠表达式展开(C++17 fold expression)
    这个是c++17支持的:

    template 
    int Add1(T t) {
        std::cout << "cur value:" << t << std::endl;
        return t;
    }
    
    template 
    void AddValue1(Args... args)
    {
        //逗号表达式+初始化列表
        //int v[] = { (Add1(args),0)... };
     
        int d = (Add1(args)+ ...);
        std::cout << "cur sum:" << d << std::endl;
    }
    
    void TestExp()
    {
        AddValue1(1, 2,7);
    }
    int main()
    {
        TestExp();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    这种折叠表达式可以进一步去看c++17的标准,回头会专门找机会详细说明一下。

    4、Tuple展开
    看一下复用Tuple展开来实现的过程:

    template 
    void DoTuple(T&& tp, F&& func)
    {
        // c++ 14 的 make_index_sequence
        DoExecFunc(std::forward(tp), std::forward(func), std::make_index_sequence::value> {});
    }
    
    template 
    void DoExecFunc(T&& tp, F&& func, std::index_sequence)
    {
        //此处的0仍然是为了填充整形数组
        std::initializer_list { (func(std::get(tp)), 0)... };
    }
    
    void TestTuple()
    {
        //注意auto的用法c++14
        DoTuple(std::make_tuple(1.1, 3, "OK"), [](const auto& value) -> void { std::cout << value << std::endl; });
    }
    int main()
    {
        TestTuple();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    通过上述的代码其实就可以清晰的看到一个可变参数函数模板是如何运作的。知其然,然后知其所以然。

    三、变参类模板

    看了变参函数模板,下面分析一下变参类模板如何实现:
    1、递归组合展开
    看一下相关代码:

    template class Base1;
    
    template<> class Base1<> {};
    template
    class Base1 //: private Base1
    {
    public:
        Base1(T t, N... n) : t_(t), b_(n...)/*Base1(n...)*/ {
            Display();
        }
        void Display() { std::cout << t_ << std::endl; }
    protected:
        T t_;
        Base1 b_;
    };
    
    void TestClassCall() {
        Base1 b('a', 2, "this is test");
    }
    int main()
    {
        TestClassCall();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    这个方式其实和下面的递归继承展开都是用的递归,只是实现手段有所不同。

    2、递归继承展开
    这个需要继承一个基础的模板类,再进行展开:

    template class Base;
    
    template<> class Base<> {};
    template
    class Base: private Base
    {
    public:
        Base(T t, N... n) : t_(t), Base(n...) {
            Display();
        }
        void Display() { std::cout << t_ << std::endl; }
    protected:
        T t_;
    };
    void TestClassCall() {
        Base b('a', 2, "this is test");
    }
    
    int main()
    {
        TestClassCall();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    这个和函数类似,通过不断的展开最终继承到一个空的模板中后,得到整体的继续体系实现展开。在c++11中使用的就是这种展开方式。

    3、Tuple展开
    这个和函数稍有不同:

    template
    class Base2
    {
    public:
    	static void Display(const std::tuple& t)
    	{
    		std::cout << "value = " << std::get(t) << std::endl;
    		Base2::Display(t);  
    	}
    };
    
    //偏特化
    template 
    class Base2< max, max, T...>
    {
    public:
    	static void Display(const std::tuple& t)
    	{
    		std::cout << "is end" << std::endl;
    	}
    };
    
    
    
    template 
    void DisplayFunc(const std::tuple& t)  //可变参函数模板
    {
    	Base2<0, sizeof...(T), T...>::Display(t);
    }
    
    void CallTuple()
    {
    	std::tuple t(12.5f, 100, 52);
    	DisplayFunc(t);
    
    }
    int main()
    {
        CallTuple();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    4、模板终止递归(偏特化)
    上面1有了一个空模板例子终止递归,下面再给一个模板参数终止递归的例子:

    //偏特化
    template
    class PartialSpec {
    public:
        enum 
        {
            value = PartialSpec::value + PartialSpec::value
            //value =  PartialSpec::value //或者此处
        };
       
    };
    
    //终止递归0--这个需要修改基本定义处不能有一个参数,即只能有变参
    template <> class PartialSpec<>
    { 
    public:
        enum { value = 0 }; 
    };
    //递归调用终止的偏特化--1
    template
    class PartialSpec {
    public:
        enum { value = sizeof(E) };
    };
    
    //递归调用终止的偏特化--2
    template
    class PartialSpec
    {
    public:
        enum { value = sizeof(T) + sizeof(R) };
    };
    
    void CallPs()
    {
        PartialSpec ps;
        std::cout << ps.value << std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    需要说明的是无论是函数模板还是类模板,都可以通过指定的条件来终止递归,可以是0,1,2…个参数(上面的终止递归可分别注释测试)。这个一定要引起注意。可以看一个1和4两个例程,一个是0个,一个是1个。当然也可以继续写其它参数数量的。也就是说,只要写一个变参模板,只要处理好结束的情况,也就是递归终结的情况,都可以实现。掌握了这一点,就明白了网上各种变参的编写模式的不同的原因了。

    5、基类变参
    在c++中,基类也可以是变参,如下代码:

    class B1 {
    public:
        void Display1() {
            std::cout << "This is B1." << std::endl;
        }
    };
    
    class B2 {
    public:
        void Display2() {
            std::cout << "This is B2" << std::endl;
        }
    };
    
    template
    class BN : public Bases...
    {};
    
    void CallMul() 
    {
        BN b;
        b.Display1();
        b.Display2();
    }
    int main()
    {
        CallMul();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    不过多重继承一般来说,是在开发中禁止使用的。这玩意儿坑太多,所以这个可以忽略,但要明白这么做是可以的就行。

    四、总结

    经过分析可以知道,技术总是在进步,开发的实际情况不断的变化,新需求的不断提出,这些都是推动技术不断向前的一个重要前提。越是活跃就越是不稳定,越是不稳定,就越欣欣向荣。所以在c++的技术迭代过程中可以看到不断的吸取Boost或者其它相关的提交的新的技术。唯有与时俱进,才不会被淘汰。
    技术,人,社会,国家莫不如是。

  • 相关阅读:
    【STM32 CubeMX】移植u8g2(一次成功)
    数据湖技术之 Hudi 集成 Flink
    Go语言基础入门
    编写函数isprime(int a),用来判断自变量a是否为素数,若是素数,函数返回整数1,否则返回0
    外文文献查找技巧方法有哪些
    【Spring】aop的底层原理
    JSON一些注意语法点
    前端一面高频react面试题(持续更新中)
    01. 板载硬件资源和开发环境
    电子学会C/C++编程等级考试2022年06月(一级)真题解析
  • 原文地址:https://blog.csdn.net/fpcc/article/details/128185614