March 15, 2024

函数模板的匹配

一、介绍和说明

在本文会尝试着把函数的重载以及模板自动推导等方法结合起来,一起分析模板函数的匹配的方式和原则。在普通的函数重载和普通的模板函数中,都比较容易理解调用哪一类,但在一些较为少用或者复杂的情况下,可能会发现一些特别的情况。这篇文章会针对这些情况进行一些具体的分析,并尝试着把一些疑惑解答分析出来。

二、函数的重载

函数的重载本身是比较简单的,即函数文件名相同但参数不同(注意,返回值不同不可以做为重载的判定),这里的参数不同有一个注意点,它指参数的个数不同或者类型不同,另外一个比较容易被忽略的是指参数的类型不同,是指按顺序(C/C++默认自右向左压栈参数)索引的严格不同。也就是说如果三个参数三个类型,只要排序不同,便认为是重载。

普通函数的重载需要注意的是auto的引入,这需要一个推导的过程,不过在实际应用中,一般很少使用auto做为模板参数,而且即使推导也相对来说比较模板要简单很多。

函数的重载麻烦在模板函数的重载,或者函数模板的实例化形成与普通函数共存时的匹配问题。

函数模板有几种方式被实例化:

1、全特化

非常遗憾的告诉大家,函数模板全特化后的函数,并不参与重载。这也是在大牛们的文章中“T.144: Don’t specialize function templates” 和“Why Not Specialize Function Templates? ”中都不建议使用函数模板的全特化而是强烈推荐使用普通函数的重载。

当然,如果一定要使用特化的方法,推荐是使用类模板封装这个函数模板,再进行类似的操作。

2、显示实例化

显示实例已经类似于普通函数了,这个其实和写普通函数没啥质的区别了。通过一般是库的开发者为了提高编译速度而应用的一种技巧。

函数模板的偏特化没有实际意义,所以其没有偏特化的处理。这就回到了教科书上的说法:

  1. 1、优先匹配普通函数
  2. 2、普通模板函数
  3. 3、如果匹配到2,其拥有全特化模板函数,则使用其。

三、模板的偏特化问题

刚刚分析过,函数模板没有偏特化,那为什么类模板有偏特化而函数模板没有偏特化呢?在前面的文章中也分析过,相关的书籍也有说明,很简单,通过偏特化来实现函数的不同的选择,基本没有什么意义。

回顾一下偏特化,就是将模板参数的部分进行特化也就是显示指定类型。对函数来说,这和写一个重载的函数有何不同?而且还不如函数重载更容易理解和实现。下面看几个小例子:


#include <iostream>

int TestOL(int d) { std::cout << "call TestOL int: " << d << std::endl; return 0; }
void TestOL(double d) { std::cout << "call TestOL double:" << std::endl; }
int TestOL(int d, int d1) { std::cout << "call TestOL int,int " << d << "," << d1 << std::endl; return 0; }

template<typename T>
int TestOL(T t) { std::cout << "call template TestOL T:" << t << std::endl; return 0; }
template
int TestOL(T1 t1,T2 t2) { std::cout << "call template TestOL t1,t2:"<(int t, int t1);//注意,不能有实现


void TestFuncOL() {
    int d = 111;
    TestOL(100);
    TestOL(d);
    TestOL(d,100);

    double db = 3.3;
    float df = 3.f;
    TestOL(db);
    TestOL(df);
   TestOL(db,df);

   TestOL(1);//显示调用全特化版本
   TestOL(db, 6);
   TestOL(1,6);//调用显示实例化版本
}
int main()
{
    TestFuncOL();
    return 0;
}

运行结果:


call TestOL int: 100
call TestOL int: 111
call TestOL int,int 111,100
call TestOL double:
call template TestOL T:3
call template TestOL t1,t2:3.3,3
call template specialize TestOL:1
call template TestOL t1,t2:3.3,6
call template TestOL t1,t2:1,6

可以在这个小例子上自行扩大测试范围,就会有清晰的理解了。所以说偏特化在函数模板上真得是没啥用处。

另外需要注意编译通过与具体调用实现的问题,这个就涉及到模板内部的延迟加载的问题了,这个问题与重载有关系,但又有不同,有兴趣可以查看下相关的书籍。

所以说,一句话,简单着来,不要自己给自己没事儿找事儿。

四、模板的匹配

好,有了前面的铺垫,可以进行模板的匹配的分析了。

1、重载的匹配

先看一个例子:


//1:标准模板
template<typename T>
void testspec(T t) { std::cout << "call template testspec T:" << t << std::endl; }

//2:此处和4相同
template<> void testspec<>(int* t) { std::cout << "call template testspec *int, t:" << *t << std::endl; }

//3:此时调用此处
template
void testspec(T* t) { std::cout << "call template testspec *T, t:" << *t << std::endl; }

//4:此处和2相同,解开注释,并把2注释掉后会调用此处。
//template<> void testspec<>(int *t) { std::cout << "call template testspec *int, t:" << *t << std::endl; }

void TestS()
{
    int data = 10;
    int* ptr = &data;
    testspec(ptr);
}

把2和4调换一下位置,则会发现产生的结果不同,他们的运行结果如标注一中所示。原因在于二者的编译虽然会产生几乎相同的结果,但是,由于重载的产生导致的结果不同。在上面的代码中,注释未解开时,2是1的全特化,2和3此时产生重载,3更合适,故而会调用3的情况;但在注释2并打开4注释时,1和3重载,4是3的特化,所以调用4。此处一定要明白,重载时,特化的函数是不参与重载的,即可清楚。

2、返回值时的匹配

下面再看一个例子:


int TestFunc(int d) {
  std::cout << "this is test!" << std::endl;
  return d + 100;
}
template ::value>::type>
void funcPack(F &&f, Args &&...args) {
  std::function m_func = [&] { return f(args...); };
}

template 
void GetClassFromName(F &&f, Args... args) { f(std::forward(args)...); }

void TestGetNameFunc() {
  GetClassFromName(TestFunc, 100);
  funcPack(TestFunc, 200);//此处没有int会是什么样?
}

此时的匹配注意点在funcPack这个函数上,特别是R这个返回值,大家仔细想一想注释中的问题,就明白了匹配的的另外一种情况了。

3、可以隐式提升的情况

看一下例子:


template<typename T>
void getData(T t) { std::cout << "call template getData   t:" << t << std::endl; }
void getData(int t) { std::cout << "call   getData  t, t:" << t << std::endl; }
int  main() {
    short d = 9;
    getData(9);//调用普通函数

    return 0;
}

4、有CV限定符的情况

5、均为模板时,显示实例化优先

6、C++11后的decltype匹配

看下面的例子:


template <typename T1,typename T2>
auto getValue(T1 t1, T2 t2)
//->decltype(t1+t2) //c++14拖尾类型
//->typename std::decay::type  //如果想去除cv限定
{
    decltype(t1 + t2) n = t1 + t2;
    std::cout << "cur n :" << n << std::endl;
    return n;
}
int main() {
    int d1 = 10;
    double d2 = 3.3;
    getValue(d1,d2);
    return 0;
}

从上面的几点可以总结出匹配的原则来:

1、普通函数优先即完全匹配

2、完全匹配时如果可以自动转换(提升类型或者隐式转换),仍然使用普通函数

3、完全匹配时,如果有CV限定等,严格按照有无匹配(但仍然是普通函数优先)

4、如果均为模板函数,则有显示实例化的优先

5、如果均为模板函数,4之后,编译器认为哪个需要处理的情况简单哪个优先(如前面的T和T*即如此)

6、decltype匹配中,无括号的标识符的,则类型与标识符一致;如果为表达式,则只检查得到函数返回值类型并保持一致;如果表达式为左值,则为一个指向其的引用;最后,以上均不符合时,则只符合表达式的类型即可。

再次重申一次,不要自找麻烦,想写重载时不要用模板的特化来实现(因为特化不参与重载),如果写函数模板,尽量将其写成单个函数模板也就是前面提到的用类模板封装其为静态函数模板(库里很多应用)。

编写模板函数时的注意点:

1、尽量要不写参与重载的模板函数的全特化,这等于自找麻烦

2、要注意模板的显示实例化和全特化及普通函数的不同

3、注意函数模板的延迟加载

五、总结

开发的过程其实是一个思维抽象再实践的过程。这个过程只有一条正确的路,就是用最简单(或者说尽可能简单)的方式来实现功能。所有的编程技巧,其终极的目标只有一个,让功能实现变得容易和安全,让代码更容易维护。当然,理想是美好的,但实践起来可能需要取舍,比如代码安全了,可能维护就复杂一些,这就看开发者的设计目标和实际场景的要求了。

总之,就是古人常说的“大道至简”!

0 comments:

VxWorks

Blog Archive