📜  奇怪的重复模板模式(CRTP)

📅  最后修改于: 2021-09-10 02:51:40             🧑  作者: Mango

背景:
建议将 Virtual Functions 和 Runtime Polymorphism 作为先决条件。下面是一个演示运行时多态性的示例程序。

// A simple C++ program to demonstrate run-time
// polymorphism
#include 
#include 
using namespace std;
  
typedef std::chrono::high_resolution_clock Clock;
  
// To store dimensions of an image
class Dimension {
public:
    Dimension(int _X, int _Y)
    {
        mX = _X;
        mY = _Y;
    }
  
private:
    int mX, mY;
};
  
// Base class for all image types
class Image {
public:
    virtual void Draw() = 0;
    virtual Dimension GetDimensionInPixels() = 0;
  
protected:
    int dimensionX;
    int dimensionY;
};
  
// For Tiff Images
class TiffImage : public Image {
public:
    void Draw() {}
    Dimension GetDimensionInPixels()
    {
        return Dimension(dimensionX, dimensionY);
    }
};
  
// There can be more derived classes like PngImage,
// BitmapImage, etc
  
// Driver code that calls virtual function
int main()
{
    // An image type
    Image* pImage = new TiffImage;
  
    // Store time before virtual function calls
    auto then = Clock::now();
  
    // Call Draw 1000 times to make sure performance
    // is visible
    for (int i = 0; i < 1000; ++i)
        pImage->Draw();
  
    // Store time after virtual function calls
    auto now = Clock::now();
  
    cout << "Time taken: "
         << std::chrono::duration_cast(now - then).count()
         << " nanoseconds" << endl;
  
    return 0;
}

输出 :

Time taken: 2613 nanoseconds

看到这个上面的结果。

当一个方法被声明为 virtual 时,编译器会偷偷为我们做两件事:

  1. 在类对象的前 4 个字节中定义一个 VPtr
  2. 在构造函数中插入代码以初始化 VPtr 以指向 VTable

什么是 VTable 和 VPtr?
当一个方法在类中被声明为虚拟时,编译器会创建一个虚拟表(又名 VTable)并将虚拟方法的地址存储在该表中。然后创建并初始化一个虚拟指针(又名 VPtr)以指向该 VTable。一个 VTable 在类的所有实例之间共享,即编译器只创建一个 VTable 实例,以便在一个类的所有对象之间共享。该类的每个实例都有自己的 VPtr 版本。如果我们打印包含至少一个虚方法的类对象的大小,输出将是 sizeof(class data) + sizeof(VPtr)。
由于虚拟方法的地址存储在 VTable 中,因此可以操纵 VPtr 来调用那些虚拟方法,从而违反了封装原则。见下面的例子:

// A C++ program to demonstrate that we can directly
// manipulate VPtr. Note that this program is based
// on the assumption that compiler store vPtr in a
// specific way to achieve run-time polymorphism.
#include 
using namespace std;
  
#pragma pack(1)
  
// A base class with virtual function foo()
class CBase {
public:
    virtual void foo() noexcept
    {
        cout << "CBase::Foo() called" << endl;
    }
  
protected:
    int mData;
};
  
// A derived class with its own implementation
// of foo()
class CDerived : public CBase {
public:
    void foo() noexcept
    {
        cout << "CDerived::Foo() called" << endl;
    }
  
private:
    char cChar;
};
  
// Driver code
int main()
{
    // A base type pointer pointing to derived
    CBase* pBase = new CDerived;
  
    // Accessing vPtr
    int* pVPtr = *(int**)pBase;
  
    // Calling virtual method
    ((void (*)())pVPtr[0])();
  
    // Changing vPtr
    delete pBase;
    pBase = new CBase;
    pVPtr = *(int**)pBase;
  
    // Calls method for new base object
    ((void (*)())pVPtr[0])();
  
    return 0;
}

输出 :

CDerived::Foo() called
CBase::Foo() called 

我们能够访问 vPtr 并能够通过它调用虚拟方法。这里解释了对象的内存表示。

使用虚方法是否明智?
可以看出,通过基类指针,派生类方法的调用正在被调度。一切似乎都运行良好。那么问题是什么?
如果一个虚拟例程被多次调用(数十万次),它会降低系统的性能,原因是每次调用该例程时,它的地址需要通过使用 VPtr 查看 VTable 来解析。每次调用虚拟方法的额外间接(指针取消引用)使得访问 VTable 成为一项代价高昂的操作,最好尽可能避免它。

奇怪的重复模板模式(CRTP)
通过 Curiously Recurring Template Pattern (CRTP) 可以完全避免使用 VPtr 和 VTable。 CRTP 是 C++ 中的一种设计模式,其中类 X 派生自使用 X 本身作为模板参数的类模板实例化。更一般地,它被称为 F 结合多态性。

// Image program (similar to above) to demonstrate
// working of CRTP
#include 
#include 
using namespace std;
  
typedef std::chrono::high_resolution_clock Clock;
  
// To store dimensions of an image
class Dimension {
public:
    Dimension(int _X, int _Y)
    {
        mX = _X;
        mY = _Y;
    }
  
private:
    int mX, mY;
};
  
// Base class for all image types. The template
// parameter T is used to know type of derived
// class pointed by pointer.
template 
class Image {
public:
    void Draw()
    {
        // Dispatch call to exact type
        static_cast(this)->Draw();
    }
    Dimension GetDimensionInPixels()
    {
        // Dispatch call to exact type
        static_cast(this)->GetDimensionInPixels();
    }
  
protected:
    int dimensionX, dimensionY;
};
  
// For Tiff Images
class TiffImage : public Image {
public:
    void Draw()
    {
        // Uncomment this to check method dispatch
        // cout << "TiffImage::Draw() called" << endl;
    }
    Dimension GetDimensionInPixels()
    {
        return Dimension(dimensionX, dimensionY);
    }
};
  
// There can be more derived classes like PngImage,
// BitmapImage, etc
  
// Driver code
int main()
{
    // An Image type pointer pointing to Tiffimage
    Image* pImage = new TiffImage;
  
    // Store time before virtual function calls
    auto then = Clock::now();
  
    // Call Draw 1000 times to make sure performance
    // is visible
    for (int i = 0; i < 1000; ++i)
        pImage->Draw();
  
    // Store time after virtual function calls
    auto now = Clock::now();
  
    cout << "Time taken: "
         << std::chrono::duration_cast(now - then).count()
         << " nanoseconds" << endl;
  
    return 0;
}

输出 :

Time taken: 732 nanoseconds

看到这个上面的结果。

虚拟方法与 CRTP 基准测试
使用虚拟方法所花费的时间为 2613 纳秒。 CRTP 带来的这种(小)性能提升是因为避免了 VTable 调度的使用。请注意,性能取决于很多因素,例如使用的编译器、虚拟方法执行的操作。不同运行中的性能数字可能会有所不同,但预计 CRTP 会带来(小)性能提升。
注意:如果我们在 CRTP 中打印类的大小,可以看到 VPtr 不再保留 4 字节的内存。

cout << sizeof(Image) << endl; 

CRTP 的另一个用例是,当需要访问基类成员函数中的派生类对象时,则必须使用 CRTP。

#include 
#include 
using namespace std;
  
template 
  
class Base {
public:
    int accessDerivedData() // Parsing json object
    {
        // this will call the respective derived class object.
        auto derived = static_cast(this);
  
        // some generic parsing logic for any json object
        // then call derived objects to set parsed values
        derived->implimentation();
        derived->display();
    }
};
  
class Derived1 : public Base // jsonMessage1
{
public:
    int data1;
    Derived1() { cout << "Derived1 constr" << endl; }
    void display()
    {
        cout << " data1:" << data1 << endl;
    }
    void implimentation()
    {
        this->data1 = 8900;
    }
};
  
class Derived2 : public Base // jsonMessage2
{
public:
    int data2;
    Derived2() { cout << "Derived2 constr" << endl; }
    void display()
    {
        cout << " data2:" << data2 << endl;
    }
    void implimentation()
    {
        this->data2 = 898;
    }
};
  
int main()
{
    auto obj1 = new Derived1;
    obj1->accessDerivedData();
  
    auto obj2 = new Derived2;
    obj2->accessDerivedData();
}

问题?让他们继续来。我们很乐意回答。

参考)
https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern