04-----C++11可变模版参数的妙用--泛化之美

2023-05-16

1 概述

C++11的新特性–可变模版参数(variadic templates)是C++11新增的最强大的特性之一,它对参数进行了高度泛化,它能表示0到任意个数、任意类型的参数。相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以它也是C++11中最难理解和掌握的特性之一。虽然掌握可变模版参数有一定难度,但是它却是C++11中最有意思的一个特性,本文希望带领读者由浅入深的认识和掌握这一特性,同时也会通过一些实例来展示可变参数模版的一些用法。

2 可变模版参数的展开

可变参数模板和普通模板的语义是一样的,只是写法上稍有区别,声明可变参数模板时需要在typename或class后面带上省略号“…”,然后在形参声明时需要在类型后面加三点,说明它是一个特殊的可变模板参数,一个典型的可变模版参数的定义是这样的:

template <class... T>
void f(T... args);

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。
我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。

可变模版参数和普通的模版参数语义是一致的,所以可以应用于函数和类,即可变模版参数函数和可变模版参数类,然而,模版函数不支持偏特化,所以可变模版参数函数和可变模版参数类展开可变模版参数的方法还不尽相同。

2.1可变模版参数函数

展开可变模版参数函数的方法一般有两种:

  • 1)一种是通过递归函数来展开参数包。
  • 2)另外一种是通过逗号表达式来展开参数包。下面来看看如何用这两种方法来展开参数包。

2.1.1递归函数方式展开参数包

通过递归函数展开参数包,需要提供两个条件:

  • 1)一个参数包展开的函数。
  • 2)一个递归终止函数,递归终止函数正是用来终止递归的,来看看下面的例子。
#include <iostream>
using namespace std;

//递归终止函数
void print()
{
   cout << "empty" << endl;
}

//展开函数
template <class T, class ...Args>
void print(T head, Args... rest)
{
   /*
   		1)首先传参方面,head=1,然后rest参数包保存着2、3、4。
   		2)然后开始调用,首先输出1,然后传参数包,递归调用本函数即print(rest...)语句,接着输出2,3,4,到参数包为0时,刚好与终止函数的形参个数0一样,
           调用终止函数输出empty后,递归终止。
   */
   cout << "parameter " << head << endl;
   print(rest...);
}
 
int test06(void)
{
   print(1,2,3,4);
   return 0;
}

递归调用过程是这样的,每次参数包少一个参数:

print(1,2,3,4);
print(2,3,4);
print(3,4);
print(4);
print();

结果:
在这里插入图片描述

上面的递归终止函数还可以写成这样:


template <class T>
void print(T t)
{
   cout << t << endl;
}

修改递归终止函数后,上例中的调用过程是这样的:

print(1,2,3,4);
print(2,3,4);
print(3,4);
print(4);

例如递归终止函数的参数为1个时的例子:

//1 递归终止函数(必须是重载形式),1个参数代表:参数包剩余1个参数时,就准备结束,然后调用此函数,空则说明参数包为空时才调用此函数结束。
template<typename T>
T sum(T t)
{
    std::cout<<"aa"<<std::endl;
    return t;
}

//2 展开函数
template<typename T, typename... Types>
T sum (T first, Types... rest)
{
    /*
        这里的运行方式是:
        1)首先第一次调进来,first=1,而此时rest参数包保存着1后面的参数,也就是2、3、4。
        2)然后return,但是由于需要运算sum<T>(rest...),所以会继续递归调用本函数(注,不是终止函数)。
        3)根据第2)点,first=2传进来,rest参数包剩余3、4。继续递归。
        4)根据第3)点,first=3传进来,rest参数包剩余4。注意,由于此时参数包rest剩下一个参数,所以会去调用终止函数。所以打印aa,并且返回4.
        5)接着函数栈一层层返回,首先是3+4=7,返回到2,first+sum<T>(rest...)=2+7=9;
        6)继续,first+sum<T>(rest...)=1+9=10。
    */
    std::cout<<"bb: "<<first<<std::endl;
    return first + sum<T>(rest...);
}

void test07(void)
{
    auto s = sum(1,2,3,4);
    std::cout<<"s: "<<s<<std::endl; // output: 10
}

sum在展开参数包的过程中将各个参数相加求和,参数的展开方式和前面的打印参数包的方式是一样的。所以结果为:
在这里插入图片描述

2.1.2逗号表达式展开参数包

递归函数展开参数包是一种标准做法,也比较好理解,但也有一个缺点,就是必须要一个重载的递归终止函数,即必须要有一个同名的终止函数来终止递归,这样可能会感觉稍有不便。
有没有一种更简单的方式呢?其实还有一种方法可以不通过递归方式来展开参数包,这种方式需要借助逗号表达式和初始化列表。比如前面print的例子可以改成这样:

template <class T>
void printarg(T t)
{
   cout << t << endl;
}
 
template <class ...Args>
void expand(Args... args)
{
    /*
        1)首先参数包args保存着1、2、3、4。
        2)然后第一次调用printarg打印出1,printarg不返回值,但是这里不关心,因为此时逗号表达式为:(xxx, 0)。所以无论是任何值,逗号表达式最终都是返回0.
        3)传入2、3、4,与传入1同理,最终结果是打印2、3、4。然后数组内部运算结果都是0,所以参数包有多少个元素,就有多少个0,这里是4个。
    */
    int arr[] = {(printarg(args), 0)...};

    //打印验证:数组元素全被初始化成0.
    for(auto i = 0; i < sizeof(arr)/sizeof(arr[0]); i++){
        std::cout<<arr[0]<<std::endl;
    }
    
}

void test08(void)
{
    expand(1,2,3,4);;
}

在这里插入图片描述
这个例子将分别打印出1,2,3,4四个数字,然后输出数组中的元素。
这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。
这种就地展开参数包的方式实现的关键是逗号表达式。因为我们知道逗号表达式会按顺序执行逗号前面的表达式,并最终取值为最后一个表达式的值。

expand函数中的逗号表达式:(printarg(args),0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。

同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args),,0)…}将会展开成((printarg(arg1),0),,(printarg(arg2),0), (printarg(arg3),0),etc… ),最终会创建sizeof…(Args)个元素值都为0的数组arr。

由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。我们可以把上面的例子再进一步改进一下,将函数作为参数,就可以支持lambda表达式了,从而可以少写一个递归终止函数了,具体代码如下:

//使用将函数作为模板参数,就可以传入lambda表达式,从而可以省去一个函数定义。
template<class F, class... Args>
void expand(const F& f, Args&&...args) 
{
    /*
        1)首先lambda传给f,后面的参数传给参数包args。
        2)然后调用f函数,即(f(std::forward< Args>(args)), 0)...;
        3)由于上面的表达式是一个参数包形式的调用方式(后面可以看到三个点...),所以会先调用完参数包里面的参数,直至参数包参数为0.
        4)所以最终调用3次,每一次传0进队列。
    */
    auto li = initializer_list<int>{(f(std::forward< Args>(args)), 0)...};//这里用到了完美转发将参数包转给f,这里的f是lambda。

    // 验证数组是否全是0.
    for(auto it = li.begin(); it != li.end(); it++){
        std::cout<<"list init value is: " << *it << std::endl;
    }
}

void test09(void)
{
    expand([](int i){cout<<i<<endl;}, 1, 2, 3);
}

结果:
在这里插入图片描述

2.2 可变模版参数类

可变参数模板类是一个带可变模板参数的模板类,比如C++11中的元祖std::tuple就是一个可变模板类,它的定义如下:

template< class... Types >
class tuple;

这个可变参数模板类可以携带任意类型任意个数的模板参数:

std::tuple<int> tp1 = std::make_tuple(1);
std::tuple<int, double> tp2 = std::make_tuple(1, 2.5);
std::tuple<int, double, string> tp3 = std::make_tuple(1, 2.5, “”);

可变参数模板的模板参数个数可以为0个,所以下面的定义也是也是合法的:

std::tuple<> tp;

可变参数模板类的参数包展开的方式和可变参数模板函数的展开方式不同,可变参数模板类的参数包展开需要通过模板特化和继承方式去展开,展开方式比可变参数模板函数要复杂。下面我们来看一下展开可变模版参数类中的参数包的方法。

2.2.1 模版偏特化和递归方式来展开参数包

可变参数模板类的展开一般需要定义两到三个类,包括类声明和偏特化的模板类。如下方式定义了一个基本的可变参数模板类:

//1 前向声明
template<typename... Args>
struct Sum;
 
//2 基本定义
template<typename First, typename... Rest>
struct Sum<First, Rest...>
{
    /**
     * 以Sum<int,double,short> s2;多参类型传入为例,编译时会发生以下过程:
     * 1)int传给First,double、short传给参数包Rest。
     * 2)此时Sum<First>::value=Sum<int>::value,调用递归终止后,返回4.
     * 3)Sum<Rest...>::value则继续递归调用本类(不是调用递归终止),此时First为double,Rest为short。
     *      所以都是一个参数,那么运算结果为:8+2=10。
     * 4)返回上一步,10+4=14.
     * 大致过程就是这样,因为是编译时就生成所以没法打印去具体观察。
    */
    enum { value = Sum<First>::value + Sum<Rest...>::value };
};
// 本类的作用就是求出每个模板具体类的模板参数列表所占的字节数。例如上面的14.
 
//3 递归终止
template<typename Last>
struct Sum<Last>
{
    // cout<<"Last"<<endl;
    // enum { value = sizeof (Last)+4 };虽然没法打印,但是这里调试可以看到,当加4后,结果14变成26,
    // 说明本终止函数是被调用了3次,而并非只调用一次即只在参数包为1个参数时调用,说明基本定义中的步骤是成立的。
    enum { value = sizeof (Last) };
};

void test10(){
    Sum<int> s1;
    cout<<"sizeof(s1) sum: "<<s1.value<<endl;   // output: 4
    Sum<int,double,short> s2;   
    cout<<"sizeof(s2) sum: "<<s2.value<<endl;   // output 14
}

结果:
在这里插入图片描述

这个Sum类的作用是在编译期计算出参数包中参数类型的size之和,通过sum<int,double,short>::value就可以获取这3个类型的size之和为14。这是一个简单的通过可变参数模板类计算的例子,可以看到一个基本的可变参数模板应用类由三部分组成。

  • 1)第一部分是:
template<typename... Args> 
struct sum

它是前向声明,声明这个sum类是一个可变参数模板类。

  • 2)第二部分是类的定义:
template<typename First, typename... Rest>
struct Sum<First, Rest...>
{
    enum { value = Sum<First>::value + Sum<Rest...>::value };
};

它定义了一个部分展开的可变模参数模板类,告诉编译器如何递归展开参数包。

  • 3)第三部分是特化的递归终止类,通过这个特化的类来终止递归。
//递归终止
template<typename Last>
struct Sum<Last>
{
    enum { value = sizeof (Last) };
};

这个前向声明要求sum的模板参数至少有一个,因为可变参数模板中的模板参数可以有0个,有时候0个模板参数没有意义,就可以通过上面的声明方式来限定模板参数不能为0个。实际上我们可以将First去掉,即可做到与tulp元祖一样,使模板参数可以是0个参数。
上面的这种三段式的定义也可以改为两段式的,可以将前向声明去掉,这样定义:

template<typename First, typename... Rest>
struct Sum
//注意上面的Sum后面,没有声明时,后面是不需要提供模板参数类型的。
{
    enum { value = Sum<First>::value + Sum<Rest...>::value };
};
 
template<typename Last>
struct Sum<Last>
{
    enum{ value = sizeof(Last) };
};

void test11()
{
    Sum<int> s1;
    cout<<"sizeof(s1) sum: "<<s1.value<<endl;   // output: 4
    Sum<int,double,short> s2;   
    cout<<"sizeof(s2) sum: "<<s2.value<<endl;   // output 14
}

输出同样的结果。
在这里插入图片描述

上面的方式只要一个基本的模板类定义和一个特化的终止函数就行了,而且限定了模板参数至少有一个。

递归终止模板类可以有多种写法,比如上例的递归终止模板类还可以这样写(但是这个在换掉上面的终止函数后,编译失败,未找到原因,先跳过,这部分应该是作者写错或者其它问题):

template<typename... Args> struct Sum;
template<typename First, typename Last>
struct Sum<First, Last>
{ 
    enum{ value = sizeof(First) + sizeof(Last) };
};

在展开到最后两个参数时终止。

但是当我实践时,遇到这样的问题:
貌似不管使用带不带前向的用法,编译时都只能通过两个模板参数列表的类,例如Sum<int,double> s1。而写成Sum<int,double,short,long> s2; 就会报错,这是什么原因呢?
个人猜可能是因为递归终止函数参数为两个时,无法正确模板特化,造成不匹配终止函数,导致编译失败?例如实参模板列表刚好2个时,刚好能匹配递归终止函数,能编译成功,而2个以外的例如4个,当第1个传进,234为参数包,然后2传进,34为参数包,此时刚好是对应终止函数,末两位能匹配,但是前面的12已经无法终止,因为单个参数是不匹配两个参数的终止函数,所以编译出错。
我是这样理解的,所以终止函数的形参基本是1参或者无参?这里可以大致了解即可。

还可以在展开到0个参数时终止:

template<>struct sum<> { enum{ value = 0 }; };

还可以使用std::integral_constant来消除枚举定义value。利用std::integral_constant可以获得编译期常量的特性,可以将前面的sum例子改为这样:

// 1 带前向声明的写法
// 前向声明
// template<typename First, typename... Args>	// 原博写错了,多写了一个First参数声明,导致类型不匹配,错误为:主模板的声明中不允许使用模板参数列表。
template<typename... Args>
struct Sum;
 
//基本定义
template<typename First, typename... Rest>
struct Sum<First, Rest...> : std::integral_constant<int, Sum<First>::value + Sum<Rest...>::value>
{
};
//constant的参1是参2的值的类型。参2实际就是一个表达式,最终通过该表达式运算得到一个值。
 
//递归终止
template<typename Last>
struct Sum<Last> : std::integral_constant<int, sizeof(Last)>
{
};

void test13(){
    Sum<int,double,short> s1;
    std::cout<<"sizeof(s1) sum:"<<s1.value<<std::endl;     // output: 14
    Sum<int,double,short,long> s2;
    std::cout<<"sizeof(s2) sum:"<<s2.value<<std::endl;     // output: 22
}


//2 或者去掉前向声明的写法
//基本定义
template<typename First, typename... Rest>
struct Sum : std::integral_constant<int, Sum<First>::value + Sum<Rest...>::value>
{
};
 
//递归终止
template<typename Last>
struct Sum<Last> : std::integral_constant<int, sizeof(Last)>
{
};

void test13(){
    Sum<int,double,short> s1;
    std::cout<<"sizeof(s1) sum:"<<s1.value<<std::endl;     // output: 14
}
// 注:1与2的区别是:1带前向声明,并且定义的时候需要写上模板参数列表,而2不需要写。

2.2.2 继承方式展开参数包

还可以通过继承方式来展开参数包,比如下面的例子就是通过继承的方式去展开参数包:

// 1 整型序列的定义
template<int...>
struct IndexSeq
{ 
};
 
// 2 定义一个继承方式,用于开始展开参数包
template<int N, int... Indexes>
struct MakeIndexes : MakeIndexes<N - 1, N - 1, Indexes...> 
{
};
 
// 3 将2模板特化,终止展开参数包的条件
template<int... Indexes>
struct MakeIndexes<0, Indexes...>
{
    typedef IndexSeq<Indexes...> type;
};
 
void test14()
{
    /**
     * MakeIndexes<3>::type的调用过程为:
     * 1)首先MakeIndexes<3>传给2的继承,此时第一次有:MakeIndexes<2, 2>{};
     * 2)然后MakeIndexes<2, 2>{}继续传给2的继承,有:MakeIndexes<1, 1, 2>{};
     * 3)然后MakeIndexes<1, 1, 2>{}继续传给2的继承,有:MakeIndexes<0, 0, 1, 2>{};
     * 此时看到,继承已经遇到了模板特化的终止条件,即3的第一个参数0。
     * 所以整个继承终止,typedef IndexSeq<Indexes...> type;将Indexes带进去后,就是:
     * typedef IndexSeq<0, 1, 2> type;
     * 故下面的T即type编译后得出该类型,对比也是一致的。
    */
    using T1 = MakeIndexes<3>::type;     // 此时T即type经过编译的类型是:T1 = IndexSeq<0, 1, 2>;
    using T2 = MakeIndexes<10>::type;    // 本例子无需运行,直接鼠标查看即可,T2 = IndexSeq<0, 1, ..., 9>;
}

展开过程是通过继承即第2点发起的,直到遇到特化的终止条件展开过程才结束。MakeIndexes<1,2,3>::type的展开过程是这样的:

// 更详细请看注释。
MakeIndexes<3> : MakeIndexes<2, 2>{}
MakeIndexes<2, 2> : MakeIndexes<1, 1, 2>{}
MakeIndexes<1, 1, 2> : MakeIndexes<0, 0, 1, 2>
{
    typedef IndexSeq<0, 1, 2> type;
}

如果不希望通过继承方式去生成整形序列,则可以通过下面的方式生成。

// 1 模板继承转换成具体语句实现,实际上与继承类似,都是递归实现。
template<int N, int... Indexes>
struct MakeIndexes3
{
    using type = typename MakeIndexes3<N - 1, N - 1, Indexes...>::type;
};
 
// 2 模板特化,终止展开参数包的条件
template<int... Indexes>
struct MakeIndexes3<0, Indexes...>
{
    typedef IndexSeq<Indexes...> type;
};

void test15(){
    /**
     * 实现过程与继承是一模一样的。
     * 1)MakeIndexes3<3>传参给N,此时进入语句中,MakeIndexes3<2, 2>::type;
     * 2)MakeIndexes3<2, 2>::type的第一个2传给N,后面的2传给参数包Indexes,有:MakeIndexes3<1, 1, 2>::type;
     * 3)MakeIndexes3<1, 1, 2>::type;继续递归,有:MakeIndexes3<0, 0, 1, 2>::type;
     * 遇到终止,此时type依旧是type = IndexSeq<0, 1, 2>。
    */
    using T = MakeIndexes3<3>::type;    // type = IndexSeq<0, 1, 2> 
}

我们上面看到了如何利用递归以及偏特化等方法来展开可变模版参数,那么实际当中我们会怎么去使用它呢?
我们可以用可变模版参数来消除一些重复的代码以及实现一些高级功能,下面我们来看看可变模版参数的一些应用。

3 可变参数模版消除重复代码

C++11之前如果要写一个泛化的工厂函数,这个工厂函数能接受任意类型的入参,并且参数个数要能满足大部分的应用需求的话,我们不得不定义很多重复的模版定义,比如下面的代码:

template<typename T>
T* Instance()
{
    return new T();
}
 
template<typename T, typename T0>
T* Instance(T0 arg0)
{
    return new T(arg0);
}
 
template<typename T, typename T0, typename T1>
T* Instance(T0 arg0, T1 arg1)
{
    return new T(arg0, arg1);
}
 
template<typename T, typename T0, typename T1, typename T2>
T* Instance(T0 arg0, T1 arg1, T2 arg2)
{
    return new T(arg0, arg1, arg2);
}

struct A
{
    A(int a){_a=a;}
    int _a;
};
 
struct B
{
    B(int a,double b){_a=a;_b=b;}
    int _a;
    double _b;
};

void test16()
{
    A* pa = Instance<A>(1);
    B* pb = Instance<B>(1,2);
    if(NULL != pa){
        delete pa;
        pa = NULL;
    }
    if(NULL != pb){
        delete pa;
        pb = NULL;
    }
}

可以看到这个泛型工厂函数存在大量的重复的模板定义,并且限定了模板参数。用可变模板参数可以消除重复,同时去掉参数个数的限制,代码很简洁, 通过可变参数模版优化后的工厂函数如下:

template<typename T, typename... Args>
//注:例如传A* pa = Instance<A>(1);时,T与Args的判断,需要将实参的模板列表推断出来再进行传参,
// 例如这里Instance<A>(1);,实际上自动推断出来是<A, int>;所以T是A,Args是int。故形参的;类型Args是int,其args参数值是1。
T* Instance(Args&&... args)
{
    return new T(std::forward<Args>(args)...);
}

void test17()
{
    A* pa = Instance<A>(1);     //被自动模板化为:A *Instance<A, int>(int &&args),A类型传给了T,值1被编译器推断出是int类型,传给了Args。
    B* pb = Instance<B>(1,2);   // 被自动模板化为:B *Instance<B, int, int>(int &&args, int &&args);
    if(NULL != pa){
        delete pa;
        pa = NULL;
    }
    if(NULL != pb){
        delete pb;
        pb = NULL;
    }
}

4 可变参数模版实现泛化的delegate

C++中没有类似C#的委托,我们可以借助可变模版参数来实现一个。C#中的委托的基本用法是这样的:

delegate int AggregateDelegate(int x, int y);//声明委托类型
 
int Add(int x, int y){return x+y;}
int Sub(int x, int y){return x-y;}
 
AggregateDelegate add = Add;
add(1,2);//调用委托对象求和
AggregateDelegate sub = Sub;
sub(2,1);// 调用委托对象相减

C#中的委托的使用需要先定义一个委托类型,这个委托类型不能泛化,即委托类型一旦声明之后就不能再用来接受其它类型的函数了,比如这样用:

int Fun(int x, int y, int z){return x+y+z;}
int Fun1(string s, string r){return s.Length+r.Length; }
AggregateDelegate fun = Fun; //编译报错,只能赋值相同类型的函数
AggregateDelegate fun1 = Fun1;//编译报错,参数类型不匹配

这里不能泛化的原因是声明委托类型的时候就限定了参数类型和个数,在C++11里不存在这个问题了,因为有了可变模版参数,它就代表了任意类型和个数的参数了,下面让我们来看一下如何实现一个功能更加泛化的C++版本的委托(这里为了简单起见只处理成员函数以及普通函数的情况,并且忽略const、volatile和const volatile成员函数的处理)。

// 一 实现类内函数万能调用的委托
// 1 创建委托类.其中T代表对象类型,R代表函数返回值类型,Args代表函数的任意形参列表的类型
template <class T, class R, typename... Args>
class  MyDelegate
{
public:
    MyDelegate(T* t, R  (T::*f)(Args...) ):m_t(t),m_f(f) {}
    R operator()(Args&&... args)
    {
            return (m_t->*m_f)(std::forward<Args>(args) ...);
    }
private:
    T* m_t;                 // 对象指针赋值
    R  (T::*m_f)(Args...);  // 函数指针赋值,该类实现类似std::thread,例如std::thread thAllOutMsg(&HandleMsg::OutAllMsg, string(tbName));
};

// 2 创建万能委托,是一个函数模板,内部调用类模板实现。
template <class T, class R, typename... Args>
MyDelegate<T, R, Args...> CreateDelegate(T* t, R (T::*f)(Args...))//R (T::*f)(Args...)实际上就是函数指针的写法,例如void (A::*f)(int),f并无要求,符合标准命名规范即可,例如改成func也可以。
{
    return MyDelegate<T, R, Args...>(t, f);
}

// 二 重载非类调用的委托,可以实现调用普通函数(对一去掉类类型T即可),但不支持lambda,可以自己进行重载实现
template <class R, typename... Args>
class  MyDelegate1
{
public:
    MyDelegate1(R  (*f)(Args...) ):m_f(f) {}
    R operator()(Args&&... args)
    {
            return (*m_f)(std::forward<Args>(args) ...);
    }
private:
    R  (*m_f)(Args...);  // 函数指针赋值
};

// 2 创建万能委托,是一个函数模板,内部调用类模板实现。
template <class R, typename... Args>
MyDelegate1<R, Args...> CreateDelegate(R (*f)(Args...))
{
    return MyDelegate1<R, Args...>(f);
}

// 类内函数
struct A
{
    void Fun(int i){cout<<i<<endl;}
    void Fun1(int i, double j){cout<<i+j<<endl;}
};

// 普通函数
void aa(int a){
    cout<<a<<endl;    
}
int bb(int a, double b){
    cout<<a+b<<endl;
}

void test18()
{
    // 1 测试类内函数调用
    A a;
    auto d = CreateDelegate(&a, &A::Fun);   //创建委托
    d(1);       //调用委托,将输出1
    auto d1 = CreateDelegate(&a, &A::Fun1);
    d1(1, 2.5); //调用委托,将输出3.5

    // 2 测试普通函数调用
    A a2;
    auto d2 = CreateDelegate(aa);
    d2(10);     //调用委托,将输出10
    A a3;
    auto d3 = CreateDelegate(bb);
    d3(5, 10.5);//调用委托,将输出15.5
}

MyDelegate实现的关键是内部定义了一个能接受任意类型和个数参数的“万能函数”:R (T::*m_f)(Args…),正是由于可变模版参数的特性,所以我们才能够让这个m_f接受任意参数。
在这里插入图片描述

5 总结

使用可变模版参数的这些技巧相信读者看了会有耳目一新之感,使用可变模版参数的关键是如何展开参数包,展开参数包的过程是很精妙的,体现了泛化之美、递归之美,正是因为它具有神奇的“魔力”,所以我们可以更泛化的去处理问题,比如用它来消除重复的模版定义,用它来定义一个能接受任意参数的“万能函数”等。其实,可变模版参数的作用远不止文中列举的那些作用,它还可以和其它C++11特性结合起来,比如type_traits、std::tuple等特性,发挥更加强大的威力,关于这些特性请参考以下博主的其它文章。

参考文章:C++11可变模版参数的妙用–泛化之美。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

04-----C++11可变模版参数的妙用--泛化之美 的相关文章

  • serialVersionUID作用

    原文出处 xff1a 未知 Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的 在进行反序列化时 xff0c JVM会把传来的字节流中的serialVersionUID与本地相应实体 xff08 类
  • linux命令与makefile学习

    linux命令与makefile学习 文件权限通配符 常用命令查看CPU 内存占用makefilegcc与g 43 43 区别 xff1a Linux上有一句话 xff1a 一切皆文件 普通文件 目录文件 d xff08 directory
  • VS在输出窗口显示信息

    输出窗口的信息传给函数 xff0c 函数内部调用系统函数OutputDebugString xff0c 就可以把调试信息打印到输出窗口 span class token keyword void span span class token
  • 使用 nlohmann 解析 json 文件

    使用 nlohmann 解析 json 文件 nlohmann json的配置json基本数据结构json文件的读取 构造与输出C 43 43 对象与nlohmann json对象的转换C 43 43 对象转换成nlohmann json对
  • ImGui实现Button高亮

    ImGui实现Button高亮 记录下在ImGui中实现Button高亮的操作 xff0c 跟着官方demo走没看到具体的实现方式 xff0c 想着渲染是不断进行的 xff0c 让下一帧绘制上次选择的状态 结果如下 xff1a 部分代码 s
  • HLSL笔记

    常量缓冲区 Constant Buffer 常量缓冲区允许C 43 43 端将数据传递给HLSL中使用 xff0c 在HLSL端 xff0c 这些传递过来的数据不可更改 xff0c 因而是常量 常量缓冲区对这种使用方式有所优化 xff0c
  • opengl shader实现Bezier曲线

    opengl shader实现Bezier曲线 顶点着色器片段着色器向shader传递数据 顶点着色器 span class token keyword const span span class token keyword char sp
  • windows创建窗口

    windows创建窗口 CreateWindowW创建窗口句柄窗口可以调节尺寸以及移动完整代码窗口的效果创建指定画面大小 xff0c 不包含窗口栏尺寸且无法调整尺寸的窗口思考 一般来讲 xff0c 要绘制或者渲染目标物体首先需要创建窗口 x
  • makefile文件解释

    makefile文件解释 makefile文件详细解释 makefile文件 CC span class token operator 61 span g 43 43 PROGRAM span class token operator 61
  • python实现自动化鼠标点击

    python实现自动化鼠标点击 span class token keyword import span pyautogui span class token keyword import span time span class toke
  • opengles共享纹理

    OpenGL ES 3 0中引入的 外部纹理 xff08 External Textures xff09 扩展 xff0c 允许将OpenGL纹理对象绑定到由外部API创建的纹理对象 xff0c 例如相机采集到的图像 视频流或其他图像数据
  • https 证书工具 Letsencrypt 简单教程

    https取代http是大势所趋 xff0c https的好处本文不在赘述 xff0c 很多公司和机构都在推进这一进程 xff0c Apple公司甚至规定 xff0c iOS上的App应用必须使用https 因此 xff0c 正是受到App
  • Linux简单命令使用笔记

    之前一直用虚拟机 xff0c 其实购买一台阿里云服务器学习linxu更加的方便快捷 阿里云服务器购买 1 electerm连接登录linux SecureCRT和SFTP 最近linux连接工具electerm 上面是两款不同的连接linu
  • 软件工程的十大模型

    1 软件生命周期模型 软件生命周期由软件定义 软件开发与运维 xff08 也称软件维护 xff09 3个时期组成 xff0c 每个时期又进一步划分成若干个阶段 问题定义 xff1a 要解决的问题是什么 xff1f 通过对客户的访问调查 xf
  • WEEK6 限时测试A - 掌握魔法の东东 II

    A 掌握魔法 东东 II 题目描述 从瑞神家打牌回来后 xff0c 东东痛定思痛 xff0c 决定苦练牌技 xff0c 终成赌神 xff01 东东有 A B 张扑克牌 每张扑克牌有一个大小 整数 xff0c 记为a xff0c 范围区间是
  • WEEK13 作业 A - TT 的神秘任务1(必做)

    A TT 的神秘任务1 xff08 必做 xff09 题目描述 这一天 xff0c TT 遇到了一个神秘人 神秘人给了两个数字 xff0c 分别表示 n 和 k xff0c 并要求 TT 给出 k 个奇偶性相同的正整数 xff0c 使得其和
  • WEEK14 作业 C - Q老师的考验(必做)

    C Q老师的考验 xff08 必做 xff09 题目描述 Q老师 对数列有一种非同一般的热爱 xff0c 尤其是优美的斐波那契数列 这一天 xff0c Q老师 为了增强大家对于斐波那契数列的理解 xff0c 决定在斐波那契的基础上创建一个新
  • 程序设计与实践 模拟题四 201809-3 元素选择器

    201809 3 元素选择器 题目描述 题解 本题是一道思维难度不大的模拟题 实现过程和思想都比较简单 xff0c 具体实现比较难 xff0c 认真仔细即可 xff08 但是自己一开始写的代码只得了80分 xff0c 又比较了其他人的代码才
  • idea中添加maven远程仓库

    idea无法自动下载依赖的解决方法 xff1a 1 xff1a 选择自己的maven目录 配置文件setting xml和仓库repository xff0c 并勾选2个Override 2 点击Runner 在VM Options那一行添
  • 详解C++中的指针结构体数组以及指向结构体变量的指针

    这篇文章主要介绍了C 43 43 中的指针结构体数组以及指向结构体变量的指针的用法 是C 43 43 入门学习中的基础知识 需要的朋友可以参考下 C 43 43 结构体数组 一个结构体变量中可以存放一组数据 xff08 如一个学生的学号 姓

随机推荐

  • bad substitution

    初接触shell脚本 xff0c 在vim中写代码 xff0c 出现了好几次 Bad substitution 我的错误有两种 xff1a 开始的的指定脚本环境 应该是 bin bash 在编译运行时 也应该用 bash 的使用错误 xff
  • Re.从零开始--基于UbuntuServer 20.04-OpenStack平台搭建_

    基于UbuntuServer 20 04 OpenStack平台搭建 前言 xff1a 本文档基于ubuntu server20 04版本和OpenStack Victoria搭建openstack环境 部署最小化Ubuntu openst
  • win10系统vvv连接不上,提示:“在连接完成前,连接被远程计算机终止”的解决办法

    进入 控制面板 网络和共享中心 更改适配器设置 右键点 vvv连接 属性 安全 选择 允许使用这些协议 xff0c 以下选项全部打勾即可 未加密的密码 质询握手身份验证协议 Microsoft CHAP Version2
  • CSP 2021 S组游记

    这是异想之旅的一篇水文 xff0c 技术无关 占个坑 xff0c 晚上更新 说说初赛 xff1a 我的竞赛老师是很重视初赛的 xff0c 整个暑假一半的时间集训 xff0c 而一半的集训时间都是面对初赛 模拟题大家做的量不同 xff0c 但
  • linux命令解压压缩rar文件的详细步骤

    一 widonds下打包rar文件并上传 yum install lrzsz rz test rar 二 下载并安装rar软件 2 1 下载 mkdir p home oldboy tools cd home oldboy tools wg
  • 配置pvst详解

    配置 pvst 在学习pvst之前 xff0c 先要学习一下stp STP生成树 思科默认有stp配置 1 选择根网桥 xff08 root bridge xff09 xff08 这个是必须的配置 xff09 选择根网桥的依据是网桥ID x
  • (真)手把手教你配置Ubuntu大数据Hadoop环境

    目录 一 前期准备 VMware tools安装 基本配置 root配置 网络配置 软件源配置 二 创建hadoop用户和文件 用户创建 小插曲 三 FTP配置 四 配置java环境及安装eclipse 安装eclipse 安装java环境
  • Ubuntu安装配置hbase完美解决方案

    目录 一 解决版本号打印失败问题 二 配置伪分布式 三 运行简单的hbase shell命令 这篇文章需要配合前一篇文章一起食用更加美味 xff08 真 xff09 手把手教你配置Ubuntu大数据Hadoop环境 一 解决版本号打印失败问
  • Linux shell实现阶乘

    bin sh read p 34 请输入想计算的数字 34 num 首先定义一个num参数接受为命令行的第一个参数 expr num 43 1 amp gt dev null 利用expr计算时参数必须是整数的原则 xff0c 如果返回零则
  • DHCP服务器搭建

    DHCP服务器搭建 安装dhcp服务器 使用yum y install dhcp 命令安装dhcp服务 修改配置文件 修改最大租约和默认租约为8天和2天 修改地址池 网关地址 以及子网掩码相关配置 总配置如下 Dhcp服务启动成功
  • idea使用技巧

    idea使用技巧 快速创建测试类 找到你想要测试的类 xff0c 按下crtl 43 shift 43 t或者右键 之后就会自动在maven的test xff08 只要是符合maven规约的文件即可 xff09 里面添加相应的测试类 测试类
  • Spring Tools Suit 4

    Spring Tools Suit 4使用手册 最近公司不让用破解版的idea xff0c 被迫转为eclipse xff0c 又因为项目大多都是spring的 xff0c 所以用spring封装好的Spring Tool Suite 4简
  • 基于Debian的发行版有哪些

    基于Debian的发行版有哪些 原创2023 04 05 08 24 贺浦力特 Debian 是一款由志愿者开发者团队创建 持续开发和维护的自由 开源的操作系统 xff0c 它以稳定 可靠和安全而著称 Debian 采用 apt 包管理工具
  • 修改JLabel背景色

    如何修改JLabel背景色 xff1f 搞笑 JLabel label 61 new JLabel label setBackground Color RED it does not work 当我们把JLabel控件加载到JPanel控件
  • Linux上启动盘制作工具大比拼

    启动盘制作工具是一种软件 xff0c 它可以帮助您将ISO镜像文件 xff0c 如Windows操作系统安装程序 xff0c 制作成可启动的U盘或光盘 xff0c 以便在需要安装操作系统或修复系统时使用 windows上制作启动盘的工具有很
  • IDEA连接SVN服务器地址拉代码,报错提示E230001: Server SSL certificate verification failed: certificate issued“验证失败问题

    问题描述 今天在使用idea拉取svn代码的时候无法操作 提示E230001 Server SSL certificate verification failed certificate issued xff0c 原因是由于SVNssl证书
  • 用c语言写一个大规模矩阵遍历的程序,在不同规模的数据下运行,比较按行遍历快还是按列遍历快。

    用c语言写一个大规模矩阵遍历的程序 xff0c 在不同规模的数据下运行 xff0c 比较按行遍历快还是按列遍历快 1 xff09 本题老师的考察点 xff1a 矩阵在计算机内存储的方式 2 xff09 解答本题时遇到的一些问题 xff1a
  • 利用Dockerfile创建指定镜像

    Dockerfile小贴士 docker允许创建自己的docker镜像 dockerfile是一个用于创建docker镜像的配置文件 xff0c 文本格式 xff0c 里面包含了创建镜像的指令 xff0c docker通过读取dockerf
  • ceph--使用ceph-deploy卸载ceph集群

    使用ceph deploy卸载ceph集群 ceph deploy purge ceph01 ceph02 ceph03 ceph deploy purgedata ceph01 ceph02 ceph03 ceph deploy forg
  • 04-----C++11可变模版参数的妙用--泛化之美

    1 概述 C 43 43 11的新特性 可变模版参数 xff08 variadic templates xff09 是C 43 43 11新增的最强大的特性之一 xff0c 它对参数进行了高度泛化 xff0c 它能表示0到任意个数 任意类型