面向对象设计与分析40讲(27)奇异递归模板确实奇异

发布时间:2023年12月18日

C++ CRTP(Curiously Recurring Template Pattern,奇异递归模板模式)是一种使用模板元编程技术的设计模式。它通过让派生类作为模板参数传递给基类,实现了静态多态性。CRTP 主要用于在编译期间实现静态多态性,避免运行时的虚函数调用开销。

CRTP 的基本思想是将派生类作为模板参数传递给基类,使得基类可以通过继承关系获取派生类的类型信息,并以此来实现一些静态多态的特性。通常的实现方式是基类作为一个模板类,其中的函数和数据成员可以使用派生类的类型信息。

下面是一个简单的示例来说明 CRTP:

template <typename Derived>
class Base {
public:
    void foo() {
        static_cast<Derived*>(this)->fooImpl();
    }
};

class Derived : public Base<Derived> {
public:
    void fooImpl() {
        // 派生类具体实现的逻辑
    }
};

在上面的代码中,Base 是一个模板类,其中的 foo() 函数通过 static_cast 将基类指针转换为派生类指针,并调用派生类的 fooImpl() 函数。这样,当我们使用 Derived 类的对象调用 foo() 函数时,实际上会调用 Derived 类中的 fooImpl() 函数。

CRTP 的优点在于它在编译期间就能够确定函数的调用路径,避免了虚函数调用的运行时开销。同时,它也可以在编译期进行类型检查,并提供更好的代码优化能力。

在普通的继承中,我们通常需要使用 dynamic_cast 来进行运行时类型检查,以确保转换的正确性。因为在普通的继承中,一个基类指针指向的对象可能是派生类的对象,也可能是其他类的对象,我们需要在运行时进行类型检查,以确保转换的正确性。

但是在 CRTP 中,模板类的参数已经确定了其派生类的类型,因此在编译期间就已经确定了转换的正确性。基类和派生类之间的关系已经通过模板参数建立好了,所以不需要进行运行时类型检查。因此,我们可以使用 static_cast 来进行转换,而不需要使用 dynamic_cast

C++ 标准库里的std::enable_shared_from_this便使用了这种模式。这种模式除了实现静态多态外,其应用场景往往是继承体系里,父类需要知道子类的类型信息,所以可以将父类改造成模板,子类的类型以模板参数的形式传入。

我们来举一个利用CRTP的经典例子:

template <typename T>
struct ObjectCounter {
    static inline int objects_created = 0;
    static inline int objects_alive = 0;

    ObjectCounter()
    {
        ++objects_created;
        ++objects_alive;
    }
    
    ObjectCounter(const ObjectCounter&)
    {
        ++objects_created;
        ++objects_alive;
    }
protected:
    ~ObjectCounter() { // objects should never be removed through pointers of this type
        --objects_alive;
    }
};

class X : ObjectCounter<X> {
    // ...
};

class Y : ObjectCounter<Y> {
    // ...
};

在这个示例中,使用了CRTP(Curiously Recurring Template Pattern)模式来实现一个计数器类。该计数器类是一个模板类counter,其中T是一个占位符类型参数。

每当创建class X的一个对象时,counter的构造函数会被调用,递增已创建和存活对象的计数。每当销毁class X的一个对象时,存活对象的计数会递减。

需要注意的是,counter和counter是两个独立的类,这就是为什么它们会分别计数X和Y的原因。在这个示例中,模板参数的存在(即counter中的T)用于区分不同的类,这也是为什么我们不能使用一个简单的非模板基类的原因。

CRTP模式通过派生类(如class X)继承模板基类(如counter),从而实现了对派生类对象的计数功能。这种模式的好处是,可以在编译期间静态解析基类的成员函数,避免了运行时的开销,并且可以为派生类提供特定的功能,而无需在每个派生类中重复实现相同的代码。

另外一个例子:解决子类链式调用问题。
考虑这样的一个打印类:

class Printer
{
public:
    Printer(ostream& pstream) : m_stream(pstream) {}
 
    template <typename T>
    Printer& print(T&& t) { m_stream << t; return *this; }
 
    template <typename T>
    Printer& println(T&& t) { m_stream << t << endl; return *this; }
private:
    ostream& m_stream;
};

我们可以使用链式调用在一行代码里进行连续操作:

Printer(myStream).println("hello").println(500);

但无法再链式调用中使用子类的方法,因为在调用链里使用的是基类(除非强制向下转型或者子类重写父类的链式调用方法):

class CoutPrinter : public Printer {
public:
    CoutPrinter() : Printer(cout) {}

    CoutPrinter& SetConsoleColor(Color c)
    {
        // ...
        return *this;
    }
};

错误提示可能是:

//                           v----- we have a 'Printer' here, not a 'CoutPrinter'
CoutPrinter().print("Hello ").SetConsoleColor(Color.red).println("Printer!"); // compile error

针对这个问题,CRTP可以很好的解决。只需父类的链式调用方法里返回子类类型的引用而非父类类型引用即可。

// Base class
template <typename ConcretePrinter>
class Printer {
public:
    Printer(ostream& pstream) : m_stream(pstream) {}
 
    template <typename T>
    ConcretePrinter& print(T&& t) {
        m_stream << t;
        return static_cast<ConcretePrinter&>(*this);
    }
 
    template <typename T>
    ConcretePrinter& println(T&& t) {
        m_stream << t << endl;
        return static_cast<ConcretePrinter&>(*this);
    }
private:
    ostream& m_stream;
};
 
// Derived class
class CoutPrinter : public Printer<CoutPrinter> {
public:
    CoutPrinter() : Printer(cout) {}
 
    CoutPrinter& SetConsoleColor(Color c) {
        // ...
        return *this;
    }
};
 
// usage
CoutPrinter().print("Hello ").SetConsoleColor(Color.red).println("Printer!");

CRTP还有一个典型应用是单例的实现,这里我们不展开,详见CRTP单例模式。

非必要情况下减少使用CRTP,主要是基于以下几点考量:

  1. 静态继承:CRTP 是通过模板的静态继承来实现的,因此无法实现动态多态。派生类的类型必须在编译期间确定,无法在运行时改变。
  2. 可读性和理解难度:不符合直觉的行为,使用 CRTP 可能使代码更加晦涩难懂,尤其对于不熟悉该模式的开发人员来说。基类模板中的奇异语法可能会导致代码可读性下降,增加理解难度。
  3. 模板实例化复杂性:当使用 CRTP 时,编译器可能会生成多个模板实例,这可能会导致编译时间增加和可执行文件大小增加。特别是当 CRTP 用于大型项目时,需要仔细考虑模板实例化的复杂性。
  4. 错误使用可能导致问题:如果使用不当,例如在派生类中不正确地使用基类模板,可能会导致隐藏的错误,例如内存泄漏或未定义的行为。
文章来源:https://blog.csdn.net/HandsomeHong/article/details/135046089
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。