第二章 编译时多态
本章介绍如何使用C++进行编译时多态机制,编译时多态相比于运行时多态,会节省运行时的开销。
2.1 函数重载机制
在C语言中除了static函数以外,不支持同名函数,再加上没有名称空间的概念,因此程序员将多个编译单元链接在一起的时候,若出现同名函数将导致链接重定义错误。通常解决办法是在函数名字前面加上模块的名字作为前缀,来避免名称冲突。在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); int main() { animal::Cat cat; feed(&cat, 1); // 使用哪个重载版本? }
2.1.1 名称查找
决策的第一阶段是进行名称查找,编译器需要在
- 成员函数名查找,当使用
. 或者-> 进行成员函数调用时,名称查找位于该成员类中的同名函数。 - 限定名称查找,当显式使用限定符
:: 进行函数调用时,例如std::sort ,查找std命名空间下的sort函数。 - 未限定名称查找,除了以上两种方式,编译器还可以根据参数依赖查找规则。
上面这个例子就是未限定名称查找,由于实参类型Cat属于
void feed(Cat* foo, int); void feed(CatLike); template<typename T> void feed(T* obj, double);
相对于另两个候选函数,第一个很容易被程序员忽视,因为我们并没有打开
std::cout << "Hello World!" << " "; std::operator<<(std::operator<<(std::cout,"Hello World!")," ");
如果不想采用这种规则,可以在函数调用时,将函数名括起来,例如:
我们需要注意的时我们模板特化函数并没有出现在候选集,这是因为名称查找阶段仅考虑普通函数和主模板函数的情况,不会考虑所有特化版本。
2.1.2 模板函数处理
模板函数无法直接调用,需要实例化,这一阶段编译器会进行处理,从而进行推导,将会替换推导后的类型。
template<typename T> void feed(T* obj, double);
这个模板函数仅一个T,而feed的调用没有显示指明实际的模板参数,因此编译器会根据实参&cat进行推导,得到模板参数T为
void feed<animal::Cat>(animal::Cat*, double);
如果模板参数推导与参数替换的过程失败,则编译器会将该模板函数从候选集中删除。那么,什么情况下模板参数会替换失败呢?考虑如下模板函数:
template<typename T> void feed(T *obj, typename T::value_type v);
该模板参数的第二个形参类型为
首先,编译器会根据实际调用的情况来进行模板参数替换,如下:
void feed<animal::Cat>(animal::Cat *obj, typename animal::Cat::value_type v);
根据Cat的定义,不存在
2.1.3 重载决议
最后一个阶段是重载决议,这个阶段得到的是一系列的模板函数与非模板函数的候选集,回顾一下当前的候选集。
void animal::feed(Cat *foo,int); // #1 void feed(CatLike); // #2 void feed<animal::Cat>(animal::Cat*, double); // #3
这个阶段可分为两步:规定可行函数集与挑选最佳可行函数。
首先,是从候选集中得到可行函数,根据函数调用的实参与候选函数的形参的数量进行规约得到可行函数集,一个可行函数必须符合如下规则。
- 如果调用函数有M个实参,那么可行函数必须得有M个形参。
- 如果候选函数少于M个形参,但最后一个参数是可变形参,则为可行函数。
- 如果候选函数多于M个形参,但是从第M+1到最后的形参都拥有默认参数,则为可行函数。
- 从C++20起,如果函数有约束,必须满足约束条件。
- 可行需要保证每个形参类型即使通过隐式转换后也能与实参类型对的上。
对于第一个候选函数来说,形参和实参类型数量都为两个,并且两个类型完全匹配,那么该候选函数可行。
对于第二个候选函数来说,
对于第三个候选函数来说,模板实例化后,第一个参数与第一个候选函数一致,第二个参数为
最后,我们发现,上面的第一个和第三个候选函数都为可行,那么,我们要从中选出最佳可行函数。接下来进入第二步。
// 目前可行函数集 void animal::feed(Cat *foo,int); void feed<animal::Cat>(animal::Cat*, double);
编译器依据每条规则逐条进行两两比较,从而决策出最佳可行函数,否则进入下一条规则。如果所有规则都不满足,那么,会引发编译错误。现如何以下几条比较重要的规则。
- 形参与实参类型最匹配、转换最少的为最佳可行函数。
- 非模板函数优于模板函数。
- 若多于两个模板实例,那么最具体的模板实例最佳。
- C++20起,若函数拥有约束,则选择约束最强的哪个。
根据第一条规则,我们就可以决策出第一个函数为最佳可行函数,因此选择该函数。
2.1.4 注意事项
根据C++标准,函数重载机制并不将函数的返回类型考虑在内,这是因为,返回类型上的差异无法决定重载决策。尽管函数(操作符)重载机制可以实现编译时多态,但并非所有重载都是为了多态而存在的。大多数情况下是为了提升表达力。
// 让复数可以进行a+b形式的操作 Complex operator+(const Complex& lhs,const Complex& rhs); // 使vector可以实现vector[5]的操作 template<typename T> T& vector<T>::operator[](size_t index);
2.1.5 再谈SFINAE
曾经,我们实现过一个
template<typename T> struct declval_protector { static constexpr bool value = false; }; template<typename T> T&& declval() { static_assert( declval_protector<T>::value, "declval应该decltype/sizeof等非求值上下文中使用!" ); }
这个实现没有考虑到注入
template<typename T> T declval(); template<typename T,typename U = T&&> U declval();
根据两个版本,构造出两个用例:
decltype(declval<void>()) 的结果为voiddecltype(declval<Point>()) 的结果为Point&&
首先对于第一个用例,在名称查找阶段,上述两个模板函数的重载都可以成为候选集,那么接下来就是模板函数处理阶段,将模板替换成void,如何获得以下两个模板实例。
void declval<void>(); void&& declval<void,void&&>();
第二个模板实例化之后,存在非法语句
再来看看第二个用例,同样地。先在名称查找阶段,这两个函数都被称为候选集,而在模板函数处理阶段,会产生如下两个模板实例。
Point declval<Point>(); Point&& declval<Point,Point&&>();
这两个实例都合法,因此这个阶段候选集没变化,接下来到了重载决议过程将发生歧义,因为这两个函数只有返回类型上的差异从而无法决策最佳版本,需要为这两个函数添加形参以进行区分,进而得到如下实现。
template<typename T> T declval(long); template<typename T,typename U = T&&> U declval(int);
但是这样要求程序员使用的时候传递一个实参去调用,提供传递
template<typename T> T declval_(long); template<typename T,typename U = T&&> U declval_(int); template<typename T> auto declval() -> decltype(declval_<T>(0)) { static_assert( declval_protector<T>::value, "declval应该decltype/sizeof等非求值上下文中使用!" ); return declval_<T>(0); // 避免编译警告 }
2.2 类型特征(Type traits)
C++通过模板来实现泛型变参,从而减轻运行时绑定开销,初看起来效率并不是最高的,因为同样的算法不会在每个数据结构上最优,比如排序链表的算法不同于排序数组的算法,有序的结构要比无序结构搜索的更快。
C++11标准库提供了
考虑这么一个场景,给定任意类型
除此之外,还能对类型进行转换,比如给定任意类型
2.2.1 Type traits谓词与变量模板
标准库提供的type traits谓词命名时is_为前缀,通过访问静态成员常量
static_assert(is_integral<int>::value); // true static_assert(!is_integral<float>::value); // false static_assert(is_floating_point<double>::value); // true static_assert(is_class<struct Point>::value); // true static_assert(!is_same<int,double>::value); // false
根据名字就可以猜测其含义,C++17提供了更简洁的方式来表达
template <typename _Tp> inline constexpr bool is_integral_v = is_integral<_Tp>::value; template <typename _Tp> inline constexpr bool is_floating_point_v = is_floating_point<_Tp>::value;
使用变量模板的一个好处是提供了更简短的访问方式,这点和类型别名很相似。定义变量模板与定义普通变量很类似,唯一不同的是定义变量模板可以带上模板参数。变量模板可以进行编译时的表达式计算,考虑如下代码。
template<char c> constexpr bool is_digit = (c >= '0' && c <= '9'); static_assert(!is_digit<'x'>); static_assert(is_digit<'0'>);
那么,我们如何使用这种方式来实现斐波那契函数,参考如下代码。
template<size_t N> inline constexpr size_t fibonacci = fibonacci<N - 1> + fibonacci<N - 2>; template<> inline constexpr size_t fibonacci<0> = 0; template<> inline constexpr size_t fibonacci<1> = 1; cout << fibonacci<100> << endl;
首先,定义一个基本变量模板
2.2.2 类型转换
标准库
int main() { static_assert(is_same_v<typename remove_const<const int>::type,int>); static_assert(is_same_v<typename remove_const<int>::type,int>); static_assert(is_same_v<typename add_const<int>::type,const int>); static_assert(is_same_v<typename add_pointer<int *>::type,int**>); static_assert(is_same_v<typename decay<int[5][6]>::type,int(*)[6]>); }
像
数组类型之所以能够传递给一个接受指针类型的函数,是因为在这个过程中数组类型发生了退化,变成了指针类型。
数组退化成指针意味着长度信息的丢失,换来的是可以接受任意长度数组的灵活性,而这也导致后续对指针的内存操作存在风险。好在C++20标准库提供了区间类型
void passArrayLike(span<int> container) { cout << "container.size():" << container.size() << endl; for (auto elem : container) cout << elem << " "; cout << endl; } int main() { int arr[]{ 1,2,3,4 }; vector vec{ 1,2,3,4,5 }; array arr2{ 1,2,3,4,5,6 }; passArrayLike(arr); // 4 passArrayLike(vec); // 5 passArrayLike(arr2); // 6 }
在C++11之后,也对类型变化相关的函数做了简洁处理,
2.2.3 辅助类
标准库
using Two = integral_constant<int, 2>; using Four = integral_constant<int, 4>; static_assert(Two::value * Two::value == Four::value);
其实,标准库有很多谓词,其结果都是一个布尔类型,因此标准库也提供了布尔类型的辅助类
template<bool v> using bool_constant = integral_constant<bool,v>; using true_type = integral_constant<bool,true>; using false_type = integral_constant<bool,false>;
那么最为基础的
template<typename T, T v> struct integral_constant { using type = integral_constant; using value_type = T; // 存储值的类型 static constexpr T value = v; // 对应的值 }
2.2.4 空基类优化
标准库中大量使用继承技术来复用代码而不是表达IsA的关系,并且不使用虚函数来避免运行时的多态开销,这也是STL高效的一个原因。就复用代码而言,与成员组合方式相比,后者只能通过大量委托转调来实现。
C++有些类可能是空的,而这些类实际上它们的大小占用一个字节。
struct Base {}; static_assert(sizeof(Base) == 1);
那么,我们考虑一个场景,通过组合的方式定义
struct Children { Base base; // 1 int other; // 4 }; static_assert(sizeof(Children) == 8);
这里需要考虑内存对齐的缘故,因此
struct Children : Base { int other; // 4 }; static_assert(sizeof(Children) == 4);
那么,空类既然什么都没有,我们为何要考虑呢,我们要知道,像
考虑一下标准库的
由于大多数情况下使用标准库提供的默认内存分配器是无状态的空类,如果使用成员变量的方式存储,至少浪费一个字节,而用户传递的分配器可能是有状态的非空类。如果统一派生自分配器,那么将会享受到空基类优化的效果。
当然也可以使用C++20提供的属性
2.2.5 实现Type traits
其实type traits,简单来说就是,它们是一种模板类,使用
template<typename T> struct is_floating_point : false_type {}; template<> struct is_floating_point<float> : true_type {}; template<> struct is_floating_point<double> : true_type {}; template<> struct is_floating_point<long double> : true_type {};
template<typename T,typename U> struct is_same : false_type {}; template<typename T> struct is_same<T,T> : true_type {};
那么,以上是实现对错,那么如何实现通过元函数进行输入类型和输出类型呢?
template<typename T> struct remove_const { using type = T; }; template<typename T> struct remove_const<const T> { using type = T; };
那么,在考虑一个复杂一点的,
template<bool v, typename Then, typename Else> struct conditional { using type = Then; }; template<typename Then, typename Else> struct conditional<false, Then, Else> { using type = Else; };
有了
- 类型为数组类型,退化成一维指针。
- 否则,类型为函数类型,退化为函数指针类型。
- 否则,去除类型的cv修饰符。
实现如下:
template<typename T> class decay { using U = std::remove_reference_t<T>; // 去除引用类型 using type = conditional_t<is_array_v<U>, remove_extent_t<U>* , // 去除掉[],变成指针 conditional_t<is_function_v<U>, // 如果是函数类型 add_pointer_t<U>, // 退化成函数指针 remove_cv_t<U> // 否则去除cv修饰符 > >; };
标准库还有一些元函数,是无法通过C++语法来实现的,例如是否是一个抽象类
2.2.6 类型内省
内省指的是程序运行时检查对象的类型或属性的一种能力,在C++中类型萃取的过程也可以视作内省,不过这个过程实在编译时查询与类型相关的特征。
通过使用模板类与特化方式,可以解构一个类型的各个组成部分。考虑最基本的一个一维数组类型,如何获得其长度?
template<typename T> struct array_size; template<typename E,size_t N> struct array_size<E[N]> { using value_type = E; static constexpr size_t len = N; }; static_assert(is_same_v<array_size<int[5]>::value_type,int>); static_assert(array_size<int[5]>::len == 5);
首先,声明一个模板类
再考虑一个更为复杂的场景,如何获得函数类型的各个组成部分?函数类型是由一个返回类型与多个输入类型组成。
template<typename F> struct function_trait; template<typename Ret,typename ...Args> // 偏特化 struct function_trait<Ret(Args...)> { using result_type = Ret; using args_type = tuple<Args...>; // 使用tuple来存储,tuple是可以存储多个类型的 static constexpr size_t num_of_args = sizeof...(Args); // 再通过传入的参数来获取第几个类型 template<size_t Index> using arg = tuple_element_t<Index,args_type>; // 下标也是从0开始 };
首先,先生们一个模板类
2.2.7 enable_if 元函数
C++11起标准库提供了
template<bool,typename = void> // 第二个默认为void struct enable_if {}; template<typename T> struct enable_if<true,T> { using type = T; };
考虑对数值进行判等操作的场景。如果是整数类型,则使用==进行判断,如果是浮点型类型,则考虑精度的问题。
template<typename T, enable_if_t<is_integral_v<T>>* = nullptr> bool numEq(T lhs, T rhs) { return lhs == rhs; } template<typename T, enable_if_t<is_floating_point_v<T>>* = nullptr> bool numEq(T lhs, T rhs) { return fabs(lhs-rhs) < numeric_limits<T>::epsilon(); }
第一个参数是T,但第二个参数是非类型模板参数
2.2.8 标签分发
除了
template<typename T> bool numEqImpl(T lhs,T rhs,true_type) // 针对于浮点类型 { return fabs(lhs-rhs) < numeric_limits<T>::epsilon(); } template<typename T> bool numEqImpl(T lhs,T rhs,false_type) // 其他算术类型 { return lhs == rhs; } template<typename T> auto numEq(T lhs,T rhs) -> enable_if_t<is_arithmetic_v<T>,bool> { return numEqImpl(lhs,rhs,is_floating_point<T>{}); }
标签分发技术可以实现
标准库的迭代器概念是容器和算法之间的桥梁,各种算法通过迭代器解耦具体的容器。迭代器可以作为广义的指针,行为与指针也很类似。下面有几种迭代器标签。
- 输入迭代器
input_iterator_tag 仅支持单向遍历,只能遍历一轮。 - 前向迭代器
forward_iterator_tag 和输入迭代器类似,但它可以支持多轮遍历。 - 双向迭代器
bidirectional_iterator_tag 与前向迭代器不同的时,它支持前后遍历。 - 随机访问迭代器
random_access_iterator_tag ,它支持随机访问,与原生指针类似。
仔细观察这几种标签,其实隐含了一种包含关系,
struct input_iterator_tag { }; struct forward_iterator_tag : public input_iterator_tag { }; struct bidirectional_iterator_tag : public forward_iterator_tag { }; struct random_access_iterator_tag : public bidirectional_iterator_tag { };
同时迭代器元函数
考虑给定迭代器和偏移量实现,对迭代器偏移的算法
使用标签分发来实现该算法,实现如下:
// 输入时单向迭代器或者向前迭代器 template<typename InputIter> void advanceImpl(InputIter& iter, typename iterator_traits<InputIter>::difference_type n, input_iterator_tag) { for (; n > 0; --n) ++iter; } // 输入是双向迭代器 template<typename BiDirIter> void advanceImpl(BiDirIter& iter, typename iterator_traits<BiDirIter>::difference_type n, bidirectional_iterator_tag) { if(n >= 0) for (; n > 0; --n) ++iter; else for(;n < 0;++n) --iter; } // 输入是随机访问迭代器 template<typename RandIter> void advanceImpl(RandIter& iter, typename iterator_traits<RandIter>::difference_type n, random_access_iterator_tag) { iter += n; } // 采用标签分发机制 template<typename Iterator> void advance(Iterator &iter, typename iterator_traits<Iterator>::difference_type n){ advanceImpl(iter,n,typename iterator_traits<Iterator>::iterator_category{}); }
2.2.9 if constexpr
我们重新对之前的numEq模板函数实现对数值类型的判等操作。
template<typename T> auto numEq(T lhs,T rhs) -> enable_if_t<is_arithmetic_v<T>,bool> { if constexpr (is_integral_v<T>) return lhs == rhs; else return fabs(lhs-rhs) < numeric_limits<T>::epsilon(); }
同样也可以对迭代器偏移函数
template<typename InputIter> void advance(InputIter &iter, typename iterator_traits<InputIter>::difference_type n) { using Category = typename iterator_traits<InputIter>::iterator_category; if constexpr (is_base_of_v<random_access_iterator_tag, Category>) { // 如果该标签时继承于random标签 iter += n; } else if constexpr (is_base_of_v<bidirectional_iterator_tag, Category>) { // 针对于双向迭代器 if (n >= 0) for (; n > 0; --n) ++iter; else for (; n < 0; ++n) --iter; }else { // 针对于输入和单向迭代器 for(;n > 0;--n) ++iter; } }
2.2.10 void_t 元函数
在C++17标准库中,引入元函数
template<typename...> using void_t = void;
该元函数输入的是一系列类型模板参数,如果这些类型良构,那么输出的类型将为
template<typename T,typename = void> struct HasTypeMember : false_type {}; template<typename T> struct HasTypeMember<T, void_t<typename T::type>> : true_type {}; static_assert(!HasTypeMember<int>::value); static_assert(HasTypeMember<true_type>::value);
首先,主模板声明要求输入两个类型参数,用户只需要提供一个,另一个默认为假。同时提供一个偏特化版本,当输入的类型具有
除了根据类型是否良构,还可以配合
来判断给定的类型是否拥有成员函数
template<typename T,typename = void> // 主模板 struct HasInit : false_type {}; template<typename T> // 偏特化版本 struct HasInit<T, void_t<decltype(declval<T>().OnInit())>> : true_type {}; struct Foo { void OnInit() {} }; static_assert(HasInit<Foo>::value); static_assert(!HasInit<int>::value);
2.3 奇异模板递归
**奇异递归模板模式(CRTP)**是C++模板编程的一种惯用法:把派生类作为基类的模板参数,从而让基类可以使用派生类提供的方法。这种方式初看和它的名字一样奇怪,但在一定场景下相当有用。
- 代码复用:由于子类派生于模板基类,因此可以复用基类的方法。
- 编译时多态:由于基类是一个模板类,能够获得传递进来的派生类,进而可以调用派生类的方法,达到多态的效果。并且没有虚表的开销。
2.3.1 代码复用
访问者模式是面向对象编程的一个经典的设计模式,虽然现代C++使用
这个模式的基本想法如下:假设我们拥有一个由不同种类的对象构成的对象结构,这些对象的类都有一个
在对象结构的一次访问过程中,我们遍历整个对象结构,对每一个元素都调用
考虑位文件目录树结构设计接口,用户可以通过访问者接口对文件目录进行遍历以完成一些动作。访问者和元素的接口如下。
struct Visitor { virtual void visit(VideoFile&) = 0; virtual void visit(TextFile&) = 0; virtual ~Visitor() = default; }; struct Elem { virtual void accpet(Visitor& visitor) = 0; virtual ~Elem() = default; };
这里涉及到两个多态结构的派发,即双重派发。
- 元素
Elem 接口通过运行时接受一个Visitor 对象,通过虚函数动态派发到双方具体类的实现上。 - 元素
Elem 通过函数重载机制对visit 接口进行静态派发。
struct VideoFile : Elem { void accpet(Visitor& visitor) override { visitor.visit(*this); } }; struct TextFile : Elem { void accpet(Visitor& visitor) override { visitor.visit(*this); } };
随着系统的演进,会发现有越来越多的重复的
答案是否定的,因为this类型实际上是该类的指针类型,如果放到基类中,那么this指针,将为基类的指针,丢失类型信息后无法静态绑定。
我们可以通过采用奇异模板递归模式,基类的模板类型将会参数化派生类的类型,便能在静态分发的同时又能服用代码,重构之后的代码如下。
template<typename Derived> struct AutoDispatchElem : Elem { void accept(Visitor& visitor) override { visitor.visit(static_assert<Derived&>(*this)); } }; struct VideoFile : AutoDispatchElem<VideoFile> {}; struct TextFile : AutoDispatchElem<TextFile> {};
由于模板基类
为一个类实现大小比较、判等操作时常见的行为,标准库的一些容器诸如
struct Point { int x; int y; friend bool operator==(const Point& lhs, const Point& rhs) { return std::tie(lhs.x, lhs.y) == std::tie(rhs.x, rhs.y); // 是将类型参数打包成元组 } friend bool operator<(const Point& lhs, const Point& rhs) { return std::tie(lhs.x, lhs.y) < std::tie(rhs.x, rhs.y); } friend bool operator!=(const Point& lhs, const Point& rhs) { return !(lhs == rhs); // 是将类型参数打包成元组 } friend bool operator>(const Point& lhs, const Point& rhs) { return rhs < lhs; } friend bool operator<=(const Point& lhs, const Point& rhs) { return !(lhs > rhs); } friend bool operator>=(const Point& lhs, const Point& rhs) { return !(lhs < rhs); } };
这里实现了一个
这里可以通过奇异模板递归的方式来实现,我们定义一个Comparable比较类,是一个模板参数为子类的基类。这个基类里面实现六种比较操作。但是子类必须拥有函数
template<typename Derived> struct Comparable { friend bool operator==(const Derived& lhs, const Derived& rhs) { return std::tie(lhs.x, lhs.y) == std::tie(rhs.x, rhs.y); // 是将类型参数打包成元组 } friend bool operator<(const Derived& lhs, const Derived& rhs) { return std::tie(lhs.x, lhs.y) < std::tie(rhs.x, rhs.y); } friend bool operator!=(const Derived& lhs, const Derived& rhs) { return !(lhs == rhs); // 是将类型参数打包成元组 } friend bool operator>(const Derived& lhs, const Derived& rhs) { return rhs < lhs; } friend bool operator<=(const Derived& lhs, const Derived& rhs) { return !(lhs > rhs); } friend bool operator>=(const Derived& lhs, const Derived& rhs) { return !(lhs < rhs); } }; struct Point : Comparable<Point> { int x; int y; auto tie() const { return std::tie(x, y); } };
C++20引入了三路比较操作符
下面重点介绍一下这三种类型:
- 偏序
partial_ordering 表达了比较关系中的偏序关系,即给定的任意两个对象不一定可比较。例如,给定一个对象树,假设父节点比子节点大,<=> 得到的是greater ,但不是任意两个节点都可以比较,此时它们的关系是unordered 。对于偏序关系的排序,使用拓扑排序算法将获得正确的结果。 - 弱序
weak_ordering 表达了比较关系中的全序关系,即给定的任意两个对象都可以比较,即不大于也不小于的关系被称为等价。假设长方形的类按照面积进行比较就是弱序关系,长宽分别为2和6的矩形与长宽分别为3和4的矩形比较,它们的面积相同,因此是等价的。但不相等是因为可以通过长宽区分出来并不一样。 - 强序
string_ordering 与弱序一样,考虑正方形按照面积比较的关系就是强序关系,因为面积相同的正方形,即边长也相同。
此外,
struct Point { int x; int y; friend auto operator<=>(const Point& lhs,const Point& rhs) = default; };
2.3.2 静态多态
奇异递归模板模式的另一个作用是实现编译时多态,避免虚函数的开销。由于基类能够在编译时获得派生类的类型,因此也能在编译时对派生类的函数进行派发。考虑该模式实现动物类的多态,设计如下接口并实现。
template<typename Derived> struct Animal { void bark() { static_cast<Derived&>(*this).barkImpl(); } }; class Cat : public Animal<Cat> { friend Animal; // 让基类访问 void barkImpl() { cout << "Miaowing!" << endl; } }; class Dog : public Animal<Dog> { friend Animal; // 让基类访问 void barkImpl() { cout << "Bow wow!" << endl; } }; template<typename T> void play(Animal<T>& animal) { animal.bark(); } int main() { Cat cat; play(cat); Dog dog; play(dog); }
2.3.3 enable_shared_from_this 模板类
现代C++常用智能指针来管理对象的生命周期和所有权,标准库提供了非侵入式的智能指针
异步编程有一个场景,需要在类中将该类的对象注册到某个回调类或函数中,不能简单的将
智能指针在最初构造的时候,通过编译时多态的手段来判断被构造的类是否派生于