函数重载大家都不陌生,其实这也是C++里最基本的最常用的编译期分支选择技术,可以有多个同名的函数,编译器会根据实参和形参的匹配情况来选择最合适的版本。下面引用Exceptional C++ Style关于函数重载的一个例子:
#include <complex> class Calc { public: double Twice(double); private: int Twice(int i); std::complex<float> Twice(std::complex<float> c); }; int main() { Calc c; c.Twice(21); //编译出错,因为int Twice(int i) 是private的 }
以上的代码的函数重载主要有以下几个步骤:
(1)名字查找:编译器会首先寻找至少包含一个名为Twice的实体作用域,并将其放入候选实体列表。本例中,编译器的名字查找首先是从Calc的作用域中开始,编译器会查看Calc中是否至少存在一个名为Twice的成员,如果没有,就会继续依次在其基类和外围空间中查找,直到找到一个至少具有一个候选函数的作用域。这里是在Calc类内部找到三个名为Twice的候选函数。
(2)重载决议。编译器在候选的重载函数中选出唯一的最佳匹配。因为c.Twice(21)参数是21,所以最佳匹配是Twice(int)。这种是精准匹配的情况,如果没有这种精准匹配,会尝试隐式转换后可以匹配的函数,如果仍然没有,编译报错。
(3)可访问性检查。最后编译器会检查函数的可访问性,因为Twice(int)是private,不可访问从而导致编译失败,是的,会直接失败而不是退而求其次选double的版本。
通过函数重载,本质上也就是在编译期选择了不同的代码分支。
而标签分发(tag dispatch),则是对函数重载机制的利用,和我们平常习惯的函数重载不一样的点在于,会用一个参数仅仅作为一个标签,这个标签并不参与任何代码逻辑,仅仅用于指导编译器选择合适的重载版本,这也是标签分发名称的由来,标签分发通常是为了解决参数相同但需要走不同的函数的问题,
标签参数通常都是一个空类,此项技术在stl库里面也有大量使用(c++20之后可以用concept更方便地实现)。这里来看一个迭代器的例子,我们知道stl里不同容器的迭代器的类型是不一样的,对于list这种,因为底层是双链表,不能随机访问,只能每次向前向后挪一,属于bidirectional_iterator,而vector这种,因为底层是连续内存空间,因此可以直接进行指针运算跳到需要的位置,属于random_access_iterator。
我们再来看std::advance函数的实现,如果需要前进n,对于random_access_iterator,我们是可以直接+n的,而对于普通的forward_iterator_tag,只能n次单加,因此可以利用标签分发写出类似以下的实现:
这里属于函数模板里使用标签分发,但是本质上和普通函数的重载是一样的。第三个参数就是标签,仅仅用于选择不同的函数,这个标签可以通过std::iterator_traits从迭代器中萃取出来。有了不同tag的impl函数,我们可以使用一个统一的入口函数来调impl,从而让编译器根据标签选择合适的版本。
下面是cppreference上的一个基于迭代器做自定义算法的标签分发的例子:
模板特化指的是除了主模板之外,针对特定的模板参数额外提供特定的实现,分为全特化合偏特化两种:
(1)全特化:指定所有的模板参数
(2)偏特化:指定部分模板参数
函数模板在选择过程也有重载决议,和函数重载类似,编译器也会从候选集中选取最合适的版本进行实例化,对于特化情况,编译器会优先选择特化程度最高的,从而让程序员能够在部分或者全部参数为特定类型的情况下提供不同的实现。
SFINAE(Substitution Failure Is Not An Error),即替换失败不是错误,这是一条针对函数模板重载决议的规则,详细的定义是:当模板形参在替换成显式指定的类型或推导出的类型失败时,只是从重载集中丢弃这个特化,而不会直接导致编译失败。通俗地来说就是,在编译器check模板调用的过程中,尝试使用某些版本会导致failure并不会导致编译失败,只要有能够正常用的,其余的failure会被忽略,这也是模板元编程经常用到的一个重要特性。
SFINAE是个很古老的特性,c++11之前就被大量运用了,到了c++11之后,标准库提供了更多的组件来简化该特性的使用,c++ 20之后又引入了concept来更简单地实现SFINAE能做地事情,但鉴于现在20完全不普及,SFINAE仍然有比较重要的价值。
SFINAE规则在模板的各个涉及到参数的部分都能生效,主要可以分为以下三类:
这里列两个典型的例子:
数组长度为0会被认为是failure,因此奇数偶数会分别使用这两个版本。
Int类型里没有B所以会选第二个。
这个例子里,重载版本1里需要根据二者相加的表达式来推导返回类型,但X没有重载+运算符不能相加,所以是个Failure,从而会选重载2,不然虽然2是普通函数但是由于需要隐式转换优先级不如能匹配的模板.
主模板接收两个模板参数,类型T和默认为void的类型,特化版本是对第二个类型参数进行特化为std::void_t<T&>,如果T是void,std::void_t<T&>会failure,从而match到主模板,否则特化版本优先级高,从而实现不可引用类型返回原类型,可引用类型添加引用。
constexpr是C++11引入的一个关键字,表明是编译器常量,实际使用中有两种用途,一个是修饰变量,一个是修饰函数返回值,修饰变量表明该变量是编译期常量,必须编译器就能确定,否则会报错。而constexpr函数如果传入的实参是编译器已知的,就会产生编译器结果,否则和没有constexpr修饰的函数没有区别。const表示readonly,constexpr则进一步限定在编译器已知。
C++17进而引入了if constexpr语法,可以方便地进行编译期分支选择,比如以下方式可以定义一个通用的取值函数,输入既支持指针,也支持原始值:
[参考]
1.https://en.cppreference.com/w/cpp/language/sfinae
2.https://arne-mertz.de/2016/10/tag-dispatch/
3.https://www.fluentcpp.com/2018/04/27/tag-dispatching/
4.Exceptional C++ Style
5.http://en.cppreference.com/w/cpp/language/template_specialization
6.http://en.cppreference.com/w/cpp/language/partial_specialization