编译时多态
函数重载机制
C++中支持函数重载与名称空间,使得多个同名函数成为可能
函数重载机制涉及三个阶段:
- 名称查找
- 模板函数处理
- 重载决议
namespace animal{
struct Cat{};
void feed(Cat* foo , int);
};
struct CatLike{CatLike(animal::Cat*);};
void feed(CatLike);
template<typename T>void feed(T* obj , double);
template <> void feed (animal:: Cat* obj , double d);
animal::Cat cat;
feed(&cat , 1); //将用哪个重载版本的feed?
名称查找
名称查找过程分为三类:
- 成员函数查找,使用.或->时会查找该成员类中的同名函数
- 限定名称查找,使用::,例如std::sort,会查找::左侧名称空间中的同名函数
- 未限定名称查找,根据参数依赖查找规则(ADL)查找
依据上述规则找到的候选函数:
void animal::feed(Cat* foo , int);//成员函数
void feed(CatLike);
template<typename T>void feed(T* obj , double);
注:
此时并不会考虑全特化版本,只有在第三阶段选出的最合适的版本未模板函数时才会考虑其特化版本
模板函数处理
在这一阶段编译器将会对模板函数进行处理,从而实例化使得函数可调用
template<typename T>void feed(T* obj , double);
|
|
v
template<animal::Cat>void feed(animal::Cat *, double);
如果参与替换的过程失败则编译器会将该模板函数从候选集中删除
替换失败但不会导致编译错误这个过程被成为substitution failure is not an error 简称SFINAE
重载决议
这个阶段分为两步:规约可行函数集和挑选最佳可行函数
1.规约可行函数集
- 如果调用函数有M个实参,那么可行函数必须得有M个形参
- 如果调用函数少于M个实参,但最后一个是可变参数,则为可行函数
- 如果调用函数多于M个实参,但从M+1到最后的形参都有默认参数,则为可行函数。在挑选最佳可行函数是只考虑前M个形参
- 从C++20起,如果函数有约束,则必须符合约束
//当前的可执行函数集
void animal::feed(Cat* foo , int);
template<animal::Cat>void feed(animal::Cat *, double);
2.挑选最佳可行函数
- 形参与实参类型最匹配,转换最少的为最佳可执行函数
- 非模板函数优于模板函数
- 若多于两个模板示例,那么最具体的最佳
- C++20起,若函数拥有约束,则选择约束更强的那一个
//最终选择
void animal::feed(Cat* foo , int);
类型特征 Type traits
C++的标准库提供了<type_traits>,它定义了一些编译时基于模板类的接口用于查询、修改类型的特征:输入的时类型,输出与该类型相关的属性
通过type_traits技术编译器可以回答一系列问题:它是否为数值类型?是否为函数对象?是不是指针?有没有构造函数?能不能拷贝构造等等
type_traits技术还能对类型进行变换,比如给定的类型任意T,能为这个类型添加const修饰符、添加引用或指针等。而这一切都发生在编译时,过程中没有任何运行时开销
Type traits 谓词与变量模板
谓词命名以is_为前缀,通过访问静态成员常量value得到输出结果
static_assert(std::is_integral<int>::value);//true
static_assert(! std::is_integral<float>::value);//false
static_assert(std::is_floating_point<double>::value);//true
static_assert(std::is_class<struct Point>::value);//true
static_assert(!std::is_same<int , long>::value);//false
is_integral用来判断给定的类型是否为整数类型,使用尖括号将类型输出给这个trait,通过其成员valu来输出一个bool类型的结果
is_floating_point用来判断给定的类型是否为浮点类型
is_class用来判断给定的类型是否为class、struct定义的类型
is_same用来判断给定的两个类型是否为相同类型
对于一个type traits谓词的结果,标准库约定使用value常量来存储,C++17为其预定义了一系列模板,::value的访问方式能够用_v来代替
template <typename T>constexpr bool is_integral_v = is_integral<T>::value;
template <typename T>constexpr bool is_class_v = is_class<T>::value;
类型变换
标准库有些typed traits拥有修改类型的能力:基于已有类型应用修改得到新的类型,输出类型可以通过访问type类型成员函数得到结果。
类型修改不会原地修改输入的类型,而是产生新的类型以应用这些修改
static_assert(is_same_v<typename std::remove_const<const int>::type , int>);
static_assert(is_same_v<typename std::remove_const<int>::type , int>);
static_assert(is_same_v<typename std::add_const<int>::type ,const int>);
static_assert(is_same_v<typename std::add_pointer<int **>::type , int ***>);
static_assert(is_same_v<typename std::decay<int[5][6]>::type , int(*)[6]>);
remove_const将输入的类型移除掉const修饰符并返回新的类型,如果不带const则不变
add_const将输入的类型添加const修饰符
add_pointer为输入的类型添加一级指针
decay语义为退化通过模拟函数或值语义传递时会使所有应用到的函数参数类型退化,若为引用那么应用将会去掉
在C++11引入类型别名using后typename decay<int[5] [6]>::type可变为decay_t<int[5] [6]>
下面是标准库定义的一些类型别名
template<typename T>using remove_const_t = typename remove_const<T>::type;
template<typename T>using decay_t = typename decay<T>::type;
辅助类
辅助类integral_constant将值与对应的类型包裹起来,从而能够将值转化为类型,也能从类型转换回值,实现值与类型的一一映射关系
using Two = std::integral_constant<int,2>;
using Four = std::integral_constant<int,4>;
static_assert(Two::value * Two::value == Four::value);
Tow和Four为两个类型,分别对应2,4。使用integral_constant将值转换成类型后通过value静态成员常量从类型中得到值并进行计算
标准库对应布尔类型也提供了bool_constant,实现时仅仅是integral_constant的类型别名
template<bool v>using bool_constant = integral_constant<bool , v>;
将这两个值映射成类型
uaing true_type = integral_constant<bool , true>;
uaing false_type = integral_constant<bool , false>;
空基类优化
对于空类,实际上它们的大小往往不是0,而是占用1个字符
struct Base{};
static_assert(sizeof(Base) == 1);
这是因为计算机内存取址的最小单位为1个字节
对于下面的代码,由于字段other四字节对齐,而base一字节对齐,多出来三字节空隙故最终占用8字节
struct Children{
Base base;
int other;
};
static_assert(sizeof(Children) == 8);
如果对空基类进行继承那么允许空基类占用0字节,即空基类优化
struct Children : Base{
int other;
};
static_assert(sizeof(Children) == 4);
C++20提供了[[no_unique_address]],将成员变量用它修饰后,若该成员的类为空类则同样享受被优化的结果
struct Children{
[[no_unique_address]]Base base;
int other;
};
static_assert(sizeof(Children) == 4);
类型内省
内省是指程序在运行时检查对象的类型或属性的一种能力。在C++中类型萃取也可以视作内省,这个萃取过程只是在编译时查询与类型相关的特征
如何获得函数类型的各个组成部分?函数类型由一个返回类型与多个输入类型组成。
template<typename F>struct function_triat;//元函数
template<typename Ret , typename ...Args>//模板偏特化
struct function_trait<Ret(Args...)>{
using result_type = Ret;
using args_type = tuple<args...>;
static constexpr size_t num_of_args = sizeof...(Args);
template<size_t I>using arg = tuple_element_t<I , args_type>;
};
enable_if元函数
enable_if常出现于SFINAE场景中,通过对模板函数,模板类中的类型进行谓词判断,使得程序能够选择合适的模板函数的重载版本或模板类的特化版本
enable_if接受两个模板参数,第一个参数为bool类型的值,当条件为真时输出的类型成员type的结果为第二个模板参数,否则没有类型成员type
int a;
typename std::enable_if<a == 1,int>::type *a; //当a==1时,condition为true,那么type为int,所以会声明一个int* a;
typename std::enable_if<!(a == 1),double>::type *a; //a !=1时,condition为true,那么type为double,所以会声明一个double* a;
enable_if用于函数模板中,典型应用是作为函数模板的返回类型
template<typename T>
typename std::enable_if<(sizeof(T) > 2)>::type funceb()
{
//....
}
nmsp2::funceb<int>();//void funceb(){}
nmsp2::funceb<char>();//error:未找到匹配的重载函数,条件不满足
//C++14出了这个等同上面
template<typename T>
std::enable_if_t<(sizeof(T) > 2),T> funceb()
{
T myt = {};
return myt;
}
nmsp2::funceb<int>();//int funceb(){}
//nmsp2::funceb<char>();
如果是第一条语句:如果funceb函数模板中涉及到enable_if_t中的条件成立的时候,这个funceb代表是一个类型,如果是第二条,条件不成立,那么有SFINAE的特性存在,请编译器忽略我这个funceb的这个函数模板吧。那么对第二条语句上例的funceb这个函数模板是不存在那样
标签分发
标签常常是一个空类,例如前面的辅助类true_type和false_type类型可视作标签。
关键在于将标签作用于重载函数中,根据不同的标签决议出不同的函数版本
if constexpr
与普通的if相比,它会在编译时对布尔常量表达式进行评估,并生成对应分支的代码。
void_t 元函数
元函数void_t用于检测一个给定的类型是否良构
奇异递归模板
把派生类作为基类的模板参数,从而让基类可以使用派生类提供的方法,可以实现代码复用和编译时多态
代码复用
访问者模式:
std::variant
std::visit
三路比较操作符<=>(飞船操作符)
auto operator<=>(const Point&) const = default;
自定义时有三种可用的返回类型:
std::strong_ordering:强比较,严格按照比较的顺序、方式来进行,不能从下面两个转回,特别注意的是它不区分等价值。
std::weak_ordering:弱比较,对比较的大小写,可以对等价的字符串用某种方式区别
std::partial_ordering:偏序比较,其实就是自定义,把直观上不可能比较的对象通过某种方式来进行比较。
enable_shared_from_this 模板类
参考资料:《C++20高级编程》