65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 V 表在运行时更改对象的行为模式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (13投票s)

2010年1月24日

CPOL

8分钟阅读

viewsIcon

39592

这项技术允许您使用 v-table 在运行时更改对象的多态行为。

引言

我最近发现了一种有趣的 C++ 技术,我以前从未读到过,所以我想在这里分享一下。它不是一种语言特性,但仍然很有趣,而且(至少在我看来)很有用。这项技术允许您在运行时更改对象的多态行为。

背景

首先,简要介绍一下背景。我有一个 Property 类,它提供对对象属性值的通用访问。为了提供这种功能,Property 类必须知道它所封装属性的数据类型。因此,我还有一个 DataType 类,它封装了数据类型并提供了对该类型值的通用访问。这个 DataType 类使用了标准的类多态设计,即为我们需要支持的每种数据类型(例如 DataType_intDataType_MyClass)实现了 abstract 基类 DataType。因此,我的 Property 类有一个指向 DataType 对象的引用(指针),该对象为它提供了对该类型值的通用访问。这也是策略模式的一个例子,它允许 Property 类在运行时更改其行为(即 DataType),并且是组合设计的例子(Property **拥有一个 DataType),而不是继承(Property 为其支持的每种 DataType 进行子类化)。到目前为止,我认为我走在正确的道路上。

问题出现在我创建了几个 DataType 子类并开始尝试将它们分配给 Property 时。由于 Property 有一个对 DataType 对象的引用,因此该对象必须存在于某个地方。因此,我有几个选择。我可以为每个 DataType 子类创建 Singleton 实例,并让 Property 对象引用这些 Singleton。或者,我可以动态分配一个 DataType 类的实例,并让 Property 类管理该对象的内存。后者将导致许多小内存分配,这会很慢,并可能导致堆碎片。因此,这不是理想的选择。而且,我尽可能不想保留全局变量,因此 Singleton 解决方案虽然不算糟糕,但也不是最佳选择。

我开始考虑使用函数指针结构来封装封装给定类型所需的许多行为。然而,我很快意识到,当实际上只需要一个引用来定义一组函数的类时,这将导致对象变得非常大。此时,我意识到(正如我确信您也已经意识到的那样),我需要的是一个类。该类通过单个引用(v-table)为每个实例提供一组函数。顺着这个思路,我开始将对象视为函数(方法)组的引用。如果我只是复制这个引用,那么我就可以更改对象的行为(就像我的 Property 类通过更改其 DataType 引用来更改其行为一样)。这是标准的策略设计模式。

Using the Code

我得出的解决方案如下(下面将进行解释)

 #include <cstring> // for memcpy

// Base DataType class
class DataType {
public:

    // Construction
    DataType() {}
    DataType(const DataType &newType) { setType(newType); }
 
    // Set the polymorphic behavior of this DataType object
    void setType(const DataType &newType) {
        memcpy(this, &newType, sizeof(DataType));
    }
 
    // Polymorphic behavior example
    protected: virtual int _getSizeOfType() const { return -1; }
    public: inline int getSizeOfType() const { return _getSizeOfType(); }
 
    // Polymorphic behavior example
    protected: virtual const char *_getTypeName() const { return NULL; }
    public: inline const char *getTypeName() const { return _getTypeName(); }
};
 
// Implementation of DataType for 'int'
class DataType_int : public DataType {
public:

    // Construction
    DataType_int() {}
    DataType_int(const DataType &newType) : DataType(newType) {}
 
    // Polymorphic behavior example
    protected:  virtual int _getSizeOfType() const { return sizeof(int); }
 
    // Polymorphic behavior example
    protected: virtual const char *_getTypeName() const { return "int"; }
};
 
// Implementation of DataType for 'float'
class DataType_float : public DataType {
public:

    // Construction
    DataType_float() {}
    DataType_float(const DataType &newType) : DataType(newType) {}
 
    // Polymorphic behavior example
    protected:  virtual int _getSizeOfType() const { return sizeof(float); }
 
    // Polymorphic behavior example
    protected: virtual const char *_getTypeName() const { return "float"; }
};
 
// Example
DataType myType = DataType_int();
const char *typeName = myType.getTypeName(); // returns "int"
int typeSize = myType.getSizeOfType(); // returns sizeof(int)
 
myType.setType(DataType_float());
typeName = myType.getTypeName(); // returns "float"

正如您所看到的,当我们设置类型时,我们只是使用 memcpy 使对象的 v-table 指针指向传入对象的 v-table。这会将 myType 的多态行为更改为新对象的行为!而且,我们不再需要指针、单例或动态内存分配!我们的对象大小与 v-table 指针一样,仅此而已!如果您希望在这里获得一点速度提升,可以直接使用 *((void**)this) = *((void**)&newType; 进行复制,假设您的 DataType 类没有成员(感谢 Dezhi Zhao 在下面的评论中指出了这一点)。

请记住,此技术不符合标准,因为标准对 v-table 或 v-ptr 没有任何规定(感谢下面所有指出这一点的评论者)。如果编译器以不将查找信息存储在对象内存空间中的方式实现虚拟方法,此技术将完全失败。然而,我从未听说过 C++ 编译器不是这样工作的。

此外,您可以看到我们可以在运行时随时轻松更改 myType 的类型。这为您提供了一种灵活性,即可以有一个未初始化的 DataType 对象数组,并在稍后需要时再初始化它们。对于追求性能的人来说,Dezhi Zhao 在下面还指出,这很可能会导致处理器在更改 getTypeName() 调用后立即出现分支预测失败。但这只会发生在上面的 DataType_float 版本中,因为如果处理器已经进行了预测,预测才会失败。

您可能注意到的一件事是使用了 public 代理方法(getSizeOfType),它们调用 protected 虚拟方法(_getSizeOfType)。我们需要这样做,因为编译器在知道对象的实际类型时可能会跳过 v-table 查找(与指针或引用不同,编译器不会跳过)。这完全是合理的,但会破坏我们的设置。然而,在代理内部,v-table 查找始终会发生。而且因为它们是内联的,所以它们实际上只是让编译器在 v-table 中查找正确的方法并调用它。但是请记住,我们 **没有** 移除虚拟方法查找。此设置不会以任何方式加快虚拟方法调用。事实上,我们依赖编译器查找我们的虚拟方法才能使其工作。

关于此设置需要注意的一点是 DataType 中没有成员变量。由于我们执行 memcpy 并期望两个对象的大小相同(sizeof(DataType)),因此 DataType 的任何子类都不能添加任何成员变量。您可以向 DataType 添加成员变量而不会有问题,但您 **不能** 向子类添加任何成员变量。由于我在 DataType 中不需要任何成员变量,因此这对我来说不是问题。但是,为子类添加成员变量并非不可能。您只需要使用基类提供的内存作为您成员的存储位置。例如

#include <cstring> // for memcpy

// Base DataType class
class DataType {
public:

    // Construction
    DataType() {}
    DataType(const DataType &newType) { setType(newType); }
 
    // Set the polymorphic behavior of this DataType object
    void setType(const DataType &newType) {
        memcpy(this, &newType, sizeof(DataType));
    }
 
protected:
 
    // Member data
    enum { kMemberDataBufferSize = 256, kMemberDataSize = 0 };
    char memberDataBuffer[kMemberDataBufferSize];
};
 
// My Data Type class
class DataType_MyType : public DataType {
public:

    // My base class
    typedef DataType BASECLASS;
 
    // Construction
    DataType_MyType() {}
    DataType_MyType(const DataType &newType) : DataType(newType) {}
 
    // Access myData
    inline int getExampleMember() const { return _getMemberData().exampleMember; }
    inline void setExampleMember(int newExampleMember) 
        { _getMemberData().exampleMember = newExampleMember; }
 
protected:
 
    // Member Data
    struct SMemberData {
        int exampleMember;
    };
 
    // Amount of member data buffer that we use (this class' member data +
    // all base class' member data)
    enum { kMemberDataSize = sizeof(SMemberData) + BASECLASS::kMemberDataSize };
 
    // Make sure that we don't run out of data buffer
    #define compileTimeAssert(x) typedef char _assert_##__LINE__[ ((x) ? 1 : 0) ];
    compileTimeAssert(kMemberDataSize <= kMemberDataBufferSize);
 
    // Access member data
    inline SMemberData &_getMemberData() {
        return *((SMemberData*) memberDataBuffer);
    }
    inline const SMemberData &_getMemberData() const {
        return *((const SMemberData*) memberDataBuffer);
    }
};

正如您所看到的,DataType 基类只是提供了一个数据缓冲区,子类可以使用该缓冲区来存储任何它们喜欢的成员数据。虽然这种设置有点混乱,但它显然有效,而且不需要太多的周折。

关注点

最后,一个与此技术相辅相成的绝佳技术是类型特征(Type Traits)。在我使用这个新的 DataType 设置实现我的 Property 类时,我意识到在注册方法或成员作为属性时指定 DataType 子类有点麻烦

Property propList[] = {
    Property(
        "prop1",
        DataType_Prop1(), &MyClass::getProp1,
        DataType_Prop1(), &MyClass::setProp1
    ),
 
    Property(
        "prop2",
        DataType_Prop2(), &MyClass::getProp2,
        DataType_Prop2(), &MyClass::setProp2
    ),
};

此外,此设置的类型安全性不高,因为如果我更改 MyClass::getProp1 的返回值,我将不会收到任何警告或错误,程序(最多)会在我使用该属性时崩溃。理想情况下,您应该这样声明属性

Property propList[] = {
    Property("prop1", &MyClass::getProp1, &MyClass::setProp1),
    Property("prop2", &MyClass::getProp2, &MyClass::setProp2),
};

数据类型将从方法声明中提取并转换为适当的 DataType 子类。幸运的是,我的 Property 构造函数已经看起来像这样

template <class Class, typename AccessorReturnType, typename MutatorArgType>
Property(
    const char *propertyName,
    const DataType &accessorDataType, AccessorReturnType (Class::*accessor)(),
    const DataType &mutatorDataType, void (Class::*mutator)(MutatorArgType)
) {
    set(propertyName, accessorDataType, accessor, mutatorDataType, mutator);
}

因此,我已经有了所需的数据类型:AccessorReturnTypeMutatorArgType。我只需要有一种机制将这些编译时 C++ 类型转换为运行时 DataType 子类对象。这实际上很容易通过一个称为**模板特化**的模板技巧来完成。我在这里不详细介绍,但如果您还不了解它的作用,请随时查看链接并回来。它非常强大。

基本思想是有一个未实现的模板类,或者实现为通用情况。然后,对于每种特殊情况,我们部分或完全特化我们的模板参数,并将其实现为新类,如下所示

// General case is not implemented. 
// If you give this template a type that isn't supported,
// you'll get a compiler error
template <typename CppType> struct MapCppTypeToDataType;
 
// Macro to define a template specialization that maps the given CppType to the given
// DataType. Once mapped, you can access the DataType like so:
//    MapCppTypeToDataType<int>::Type
// This should resolve to whatever type you mapped to int (DataType_int, for example).
#define MAP_DATA_TYPE(CppType, MappedDataType) \
    template <> struct MapCppTypeToDataType<CppType> { \
        typedef MappedDataType Type; \
    }
 
// Function to convert a C++ type to a DataType object
template <typename CppType>
inline DataType GetDataType() {
    return MapCppTypeToDataType<CppType>::Type();
}
 
// Example
MAP_DATA_TYPE(int, DataType_int);
DataType myDataType = GetDataType<int>(); // returns a DataType_int

您可以看到它的强大之处。现在,我们可以添加一个新的 Property 构造函数,为您计算正确的 DataType 对象

template <class Class, typename AccessorReturnType, typename MutatorArgType>
Property(
    const char *propertyName,
    AccessorReturnType (Class::*accessor)(),
    void (Class::*mutator)(MutatorArgType)
) {
    set(
        propertyName,
        GetDataType<AccessorReturnType>(), accessor,
        GetDataType<MutatorArgType>(), mutator
    );
} 

此构造函数允许您按照我们期望的方式声明属性,如下所示

Property propList[] = {
    Property("prop1", &MyClass::getProp1, &MyClass::setProp1),
    Property("prop2", &MyClass::getProp2, &MyClass::setProp2),
}; 

您可以轻松地看到,此构造函数比旧构造函数更容易、更安全。您不再需要知道您正在注册的方法的类型。编译器已经知道它,可以简单地为您完成工作。并且这种方法更安全,因为如果您现在更改 prop1 的返回类型,编译器将简单地更改使用的 DataType。而且,如果不存在支持新返回类型的 DataType,您的编译器将给您一个错误,类似于“Type was not declared in class 'MapCppTypeToDataType' with template parameters ...”。

我希望您喜欢阅读有关此技术的文章。如果您有任何评论或问题,我很想听听。感谢阅读!

附注:我不确定上面的代码片段是否可以编译。它们仅用于说明目的,而非编译。但是,如果您发现任何错误,请告知我,我会进行更正。

历史

  • 2010 年 1 月 24 日 - 原始文章。
  • 2010 年 2 月 1 日 - 修复了一些代码错误。
© . All rights reserved.