最近拜读了马里乌斯.班西拉(Marius Bancila)写的《C++20模板元编程》一书,学习了一些模板编程的技术,特此记录一下一些对个人有启发的内容。模板编程的基础就不再赘述了,主要是一些常用的模式和惯用法,也就是全书的第7章节。
1、动态多态和静态多态
动态多态通过抽象(abstraction)、封装(encapsulation)、继承(inheritance)和多态(polymophism)实现。抽象提供了对一个集合类别的公共行为的抽取能力,其目的是顶层逻辑的统一处理;继承让实现类具备了父类的一些能力,能够从顶层的逻辑对实现逻辑进行一些调度;多态是最终的目标,在顶层逻辑下对实现的细化实现。
静态多态和动态多态不一样的地方在于静态多态是在编译期对实现行为进行了多态的处理。静态多态一般通过函数重载和模板编程来实现。接下来会讲述一些关于静态多态的常用模式和惯用法,这样能更好理解。
2、奇异递归模板模式
奇异递归模板模式(CRTP,Curiously Recurring Template Patten)是一种模板的编程模式。其主要特征是父类的模板实参是子类,如下是一个例子:
/*
父类模板,实现顶层逻辑
*/
template <typename T>
struct game_unit
{
void attack()
{
static_cast<T*>(this)->do_attack(); // 注意,这里调用的是子类的普通成员函数,不是虚函数
}
};
struct knight : game_unit<knight>
{
void do_attack()
{
std::cout << "draw sward\n";}
};
struct mage : game_unit<mage>
{
void do_attack()
{
std::cout << "spell magic curse\n";}
};
CRTP之所以称为“奇异”是因为它的父类的形式依赖于子类,从逻辑上来讲,父类不该依赖于子类,就像父类不该依赖于子类的实现一样,不然要父类还有什么作用?在动态多态中,父类的作用就是抽象子类的行为。我们来分析一下为什么模板编程可以这么做。
首先,从逻辑上讲CRTP的父类实现就是依赖于子类的行为,要注意不同子类的父类是不同的父类,其行为在调用子类方法上是不一致的,但是除去调用子类具体行为的地方,其行为又是一致的。所以,从逻辑上来讲CRTP是父类实现了高层逻辑,子类实现了多态(这一点后面会和MIXINS进行对比)。
其次,从技术上来讲模板参数实际并不需要模板的实例,在子类实例化的时候,父类其实不知道子类的具体定义,它只知道它有一个子类,这个子类有一些方法(这一点做的更好一些可以通过C++20的概念(concept)进行约束)。举个例子,我们定义一个某人的爸爸模板类,我们不需要知道某人到底是谁,我们也能知道这个某人的爸爸可以实现打某人屁股这个操作,那么就可以在“某人爸爸模板父类”中实现这个“打屁股”的成员方法,打谁的屁股,被打之后是“大声呼痛”还是“泪如雨下”那是子类来实现的。
基于以上的认知,我们知道父类可以用子类的名义,也可以使用子类的成员,但是不能使用子类的编译期类型和编译期成员(链接期的可以),具体如下:
template<typename T>
struct father
{
using type = typename T::type;
};
struct son: father<son>
{
using type = int;
};
这个程序如果实例化son,那么编译期就会在父类using子类的type的时候告诉你son还没有定义。但是如下程序是可以通过编译的:
template<typename T>
struct father
{
void hit_son()
{
std::cout << "father hit son" << std::endl;
static_cast<T*>(this)->cry();
}
};
struct son: father<son>
{
using type = int;
void cry()
{
std::cout << "son cry" << std::endl;
}
};
思考一下为什么会这样?关键在于编译分为编译和链接2个步骤。第1个例子中,是在编译期(具体来说是语义分析)的时候需要确定father::type的类型,而father::type依赖于son::type,也就意味着son这个类型必须先于father被定义,而son的定义由于继承了father类,所以son同时又依赖于father的定义。很明显,这形成了一个逻辑死循环。编译器必须确定一个明确的方式,编译器会先语义分析father,发现father需要son的定义,而son还没有定义,所以报错。
对于第2个例子,可以先定义father,在编译期编译器只需要记录father在hit_son成员中会调用son::cry这个成员函数,然后继续去定义son。完成了son的定义后,链接器发现father::hit_son里面还有一个son::cry的成员函数地址没有填写,这个时候链接器会将son::cry的地址填写到father::hit_son调用的位置。逻辑完美闭环。
标准库中有一个std::enable_shared_from_this,这个类别就是使用了混入创建了“母版”shared_ptr,然后通过暴露shared_from_this使的子类的平凡指针也可以获取shared_ptr而不用担心有多次构建的shared_ptr指向同一个对象导致的多次释放问题。
总结一下,CRTP的特点就是:1、模板类作为父类,多态类作为子类;2、父类实现上层逻辑,子类实现多态逻辑;3、父类通过调用子类成员函数实现多态能力;
CRTP用于聚合设计(抽象),并且减少公共代码,在父类中聚合各个子类的工作流程,在子


829

被折叠的 条评论
为什么被折叠?



