背景:
建议将 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 时,编译器会偷偷为我们做两件事:
- 在类对象的前 4 个字节中定义一个 VPtr
- 在构造函数中插入代码以初始化 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