IE盒子

搜索
查看: 113|回复: 0

C++重载函数调用的决策机制

[复制链接]

4

主题

6

帖子

14

积分

新手上路

Rank: 1

积分
14
发表于 2022-12-9 14:39:50 | 显示全部楼层 |阅读模式
一、C++重载函数调用的决策规则

  前面我们介绍了所谓运行时多态,在性能上,它们会有运行时的开销。而C++还支持所谓编译期的多态,即编译器在编译期就帮助我们确定好要调用哪个函数,即所谓的函数重载机制
  如果您对为社么C不支持函数重载而C++支持函数重载的原因有了解的话,会知道C++修饰函数名并不是像C那样直接以函数名作为链接阶段区分不同函数的符号,而是以函数名+作用域+参数缩写作为链接阶段区分不同函数的符号,这样避免了所谓同名函数的冲突而形成函数重载。
C的函数名修饰风格:void fun()



C++的函数名修饰规则:fun(int) fun()



  这个原理解决了同名函数在链接阶段形成的符号冲突的问题,使得函数重载得以在C++中存在,但是还有一个问题,那么编译器是如何在编译期间根据我们的参数抉择出它要具体调用哪个函数的呢?这就是我们今天的话题——函数重载的决策机制。
  当然,本人知识水平有限,都是参考的这个书上的说法,如有错误和纰漏,还请见谅。
  总的来说,函数重载机制分为三个阶段:名称查找、模版函数处理、重载决议,前两个阶段会得到函数的候选集,最后一个阶段从函数的候选集中选出最为合适的版本。
  我们以下面为例,简单介绍下这个决策规则:
namespace animal {
  struct Cat {};
  void feed(Cat* foo, int);
}

/*animal::Cat*可以隐式转化为Catlike*/
struct Catlike {
  Catlike(animal::Cat*);
};

void feed(Catlike);

template <class T>
void feed(T* obj, double);
template <class T>
void feed(T obj, double);

template<>
void feed(animal::Cat* obj, double);

animal::Cat cat;

feed(&cat, 1);
1 名称查找

  第一阶段的名称查找的目的是找出所有与feed同名的函数声明与函数模版,通常来说有以下规则:

  • 成员函数名查找,当你是一个对象或对象的指针使用.或->访问成员函数时,会查找该成员类中的同名函数。
  • 限定名称查找,即显示的使用::进行函数调用时,如调用std::sort时,会在相应的类域中查找。
  • 未限定名称查找,即除了上面的调用,编译器会根据你这个参数的依赖,即所谓依赖查找规则ADL进行查找,什么意思呢,其实就是说你的参数里面有是什么命名空间的东西,我也会去那个命名空间查找。
  那么在上面这个例子里头啊,这个查找属于是未限定名称查找,它会在animal命名空间找到void animal::feed(Cat*, int);
  然后另外的三个结果是全局域查找得到的:全局的feed(Catlike);和两个模版函数template <class T> void feed(T* obj, double); template <class T> void feed(T obj, double);。
  这里提一嘴,其实这个第三条规则相当有用,它也不仅限于函数重载领域,运算符重载也有这条规则(运算符重载不过是把运算符转化为函数operator运算符()然后利用函数重载规则罢了),如果没有这条规则,下面的代码就要这样写:
// 没有using namespace std的情况下

std::cout << "hello world" << '\n';

std::operator<<(std::operator<<(std::cout, "hello world"), '\n');
  可以通过加()限制第三条规则不让其生效,即:





  这个模版特化的函数啊,并不会在这一阶段处理,它会在第三阶段如果把模版函数选择为最合适版本时,才会去根据一些规则考虑其特化版本。
2 模版函数处理

  这个阶段就是会把模版参数去实例化方便后续决策,即会把类型之类的做好推导:
template <class T>
void feed(T* obj, double);
// 实例化为
void feed(animal::Cat* obj, double);

template <class T>
void feed(T obj, double);
// 实例化为
void feed(animal::Cat* obj, double);
  不过模版类型参数的推导是有可能会失败的,比如你有个函数参数写成typename T::value_type v,如果实例化的T里头没有这个value_type,编译器就会把这个模版函数从候选集中删除,这个技巧其实蛮有用的,这样可以让编译器在某种程度上按你的想法实现所谓编译分枝,一定程度上操纵编译器的决策过程,如我们可以这样去完善我们的declval。
  所周知,void是没法引用的,所以我们之前写的那个版本的declval实现会在参数列表写一个void时发生报错:
template <class T> struct declval_protector {
        static constexpr bool value = false;
    };
    template <class T>
    T&& declval()
    {
        static_assert(declval_protector<T>::value, "declval应该只在decltype和sizeof等非求值上下文中使用!");
    }

ResTypeOfF<f, void> func()
{

}




  这里对void去补上一个没有引用的版本,并对原版本用上面的那个技巧去处理,这样void&&编译时会让编译器给我们删掉:



  然后发现对非void类型的T来说,这两个函数还有歧义,为了弥补这个,让他俩的函数参数类型不同,然后我们自己来定模版规则来匹配:
namespace scu {
    template <class T> struct declval_protector {
        static constexpr bool value = false;
    };
    template <class T, typename U = T&&>
    U declval_(int);

    template <class T>
    T declval_(long);
    template <class T>
    auto declval()->decltype(declval_<T>(0))
    {
        static_assert(declval_protector<T>::value, "declval应该只在decltype和sizeof等非求值上下文中使用!");
        return declval_<T>(0);
    }
}
  这样对普通类型来说,由于0到long会发生类型转化,而到int不会发生类型转换,所以在两个declval_都可以的情况下,它会选择第一个,从而普通类型就像原本一样正常。
3 重载决议

  这个阶段会分为两步:规定可行函数集与选择最佳的可行函数。
  我们刚刚选的函数里头,你会发现仅仅是按名字选择,参数个数到不一定匹配,所以首先我们要从那些函数里选择可行函数集合,惠泽如下:

  • 如果调用函数有M个实参,那么可行函数必须得有M个形参;
  • 如果候选函数少于M个形参,但最后一个参数是可变参数,则为可行函数;
  • 如果候选函数多于M个形参,但是从第M+1个参数开始都有默认参数,则为可行函数,在挑选最佳可行函数时只考虑其前M个形参。
  • 从C++20起,如果函数有约束,则必须符合约束。
  • 可行函数需要保证每个形参类型通过隐式类型转换后能和实参类型对的上
  所以这个阶段会淘汰那个全局函数feed(Catlike);,剩余的函数是:
void animal::feed(Cat*, int);

template <class T>
void feed(T* obj, double);
// 实例化为
void feed(animal::Cat* obj, double);

template <class T>
void feed(T obj, double);
// 实例化为
void feed(animal::Cat* obj, double);
  然后进入下一个阶段,选择最佳的模版函数,它的规则比较多,我们罗列几条规则:

  • 形参与实参最匹配、转换次数最少的为最佳可行函数;
  • 非模版函数优于模版函数
  • 若有与两个模版实例,那么最具体的模版实例最佳。
  • C++20起,若函数有约束,则选择约束最强的那一个。
  第四条我是不太懂啊,不过根据第二条我们就选择出了void animal::feed(Cat*, int)。
  不过假如没有这个函数,剩下两个模版函数pk,他俩都需要int转double,所以这点平局,但是显然T*更加的具体,因为这说明它只接受指针类型,而T没那么具体,T可以接受任意类型,所以会选择第一个模版函数,如果那个函数还有相应特化版本,也会在里头选。
  比如我们把(feed)这样就没有void animal::feed(Cat*, int),看看选择结果:



  如果这样决策不出来,就会出编译错误了。
参考文献

[1]《C++20高级编程》罗能著 机械工业出版社
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表