用于在不使用 RTTI 的情况下缓存不同派生类型的设计模式



假设我有一系列类,它们都实现相同的接口,也许是为了调度:

class Foo : public IScheduler {
public:
    Foo (Descriptor d) : IScheduler (d) {}
    /* methods */
};
class Bar : public IScheduler {
public:
    Bar (Descriptor d) : IScheduler (d) {}
    /* methods */
};

现在假设我有一个调度程序类,您可以要求为给定的描述符启动一个 IScheduler 派生类。 如果它已存在,您将获得对它的引用。 如果不存在,则创建一个新的。

一个假设的调用是这样的:

Foo & foo = scheduler->findOrCreate<Foo>(descriptor);

实现这一点需要一个映射,其键(描述符,RTTI)映射到基类指针。 然后你必须dynamic_cast. 我猜是这样的

template<class ItemType>
ItemType & Scheduler::findOrCreate(Descriptor d)
{
    auto it = _map.find(SchedulerKey (d, typeid(ItemType)));
    if (it == _map.end()) {
        ItemType * newItem = new ItemType (d);
        _map[SchedulerKey (d, typeid(ItemType))] = newItem;
        return *newItem;
    }
    ItemType * existingItem = dynamic_cast<ItemType>(it->second);
    assert(existingItem != nullptr);
    return *existingItem;
}

想知道是否有人有办法在不依赖 RTTI 的情况下获得类似的结果。 也许每种计划项目类型都可以拥有自己的地图实例? 设计模式,还是... ?

函数

或类静态成员的地址保证是唯一的(据<所知),因此您可以使用这样的地址作为键。

template <typename T>
struct Id { static void Addressed(); };
template <typename ItemType>
ItemType const& Scheduler::Get(Descriptor d) {
    using Identifier = std::pair<Descriptor, void(*)()>;
    Identifier const key = std::make_pair(d, &Id<ItemType>::Addressed);
    IScheduler*& s = _map[key];
    if (s == nullptr) { s = new ItemType{d}; }
    return static_cast<ItemType&>(*s);
}

请注意使用 operator[] 以避免双重查找并简化函数体。

这是一种方法。

添加一个纯虚拟方法IScheduler

virtual const char *getId() const =0;

然后将每个子类放入它自己的 .h.cpp 文件中,并定义函数:

virtual const char *getId() const { return __FILE__; }

此外,对于在编译时具有确切类型的模板,在同一文件中定义静态方法,您可以在没有类实例的情况下使用(AKA 静态多态性):

static const char *staticId() { return __FILE__; }

然后将其用作缓存映射键。 __FILE__符合C++标准,因此这也是便携式的。

重要说明:使用正确的字符串比较,而不仅仅是比较指针。也许返回std::string而不是char*以避免事故。从好的方面来说,您可以与任何字符串值进行比较,将它们保存到文件等,您不必仅使用这些方法返回的值。

如果你想比较指针(比如为了提高效率),你需要更多的代码来确保每个类只有一个指针值(在 .h 中添加私有静态成员变量声明,在相应的.cpp中使用 FILE 进行定义+初始化,然后返回它),并且只使用这些方法返回的值。


注意类层次结构,如果你有类似的东西

  • A继承IScheduler 必须覆盖getId()
  • A2继承A,编译器不会抱怨忘记getId()

然后,如果您想确保不会意外忘记覆盖getId(),您应该拥有

  • 抽象Abase继承IScheduler,而不定义getId()
  • 最终A继承Abase,并且必须添加getId()
  • 最终A2继承Abase,并且必须添加getId(),此外还更改了 A

(注意:final具有特殊含义的关键字标识符是C++11功能,对于早期版本,只需将其省略...

如果调度程序是单例,这将起作用。

template<typename T>
T& Scheduler::findOrCreate(Descriptor d) {
    static map<Descriptor, unique_ptr<T>> m;
    auto& p = m[d];
    if (!p) p = make_unique<T>(d);
    return *p;
}
如果调度程序

不是单例,则可以使用相同的技术拥有一个中央注册表,但将调度程序*/描述符对映射到unique_ptr。

如果您知道所有不同的IsScheduler亚型,那么绝对是的。看看Boost.Fusion,它让你创建一个地图,其键实际上是一个类型。因此,对于您的示例,我们可以执行以下操作:

typedef boost::fusion::map<
    boost::fusion::pair<Foo, std::map<Descriptor, Foo*>>,
    boost::fusion::pair<Bar, std::map<Descriptor, Bar*>>,
    ....
    > FullMap;
FullMap map_;

我们将这样使用该地图:

template <class ItemType>
ItemType& Scheduler::findOrCreate(Descriptor d)
{
    // first, we get the map based on ItemType
    std::map<Descriptor, ItemType*>& itemMap = boost::fusion::at_key<ItemType>(map_);
    // then, we look it up in there as normal
    ItemType*& item = itemMap[d];
    if (!item) item = new ItemType(d);
    return item;
}

如果尝试查找或创建未在FullMap中定义的项,则at_key将无法编译。因此,如果您需要一些真正动态的东西,您可以在其中临时添加新的调度程序,这是行不通的。但如果这不是必需的,这很好用。

static_cast ItemType* void*并将其存储在地图中。然后,在findOrCreate中,只需获取void*并将其static_castItemType*即可。

static_cast T* ->

void* -> T* 可以保证让您恢复原始指针。您已使用 typeid(ItemType) 作为密钥的一部分,因此可以保证只有在请求完全相同的类型时查找才会成功。所以这应该是安全的。

如果您还需要调度程序映射中的IScheduler*,只需存储两个指针即可。

最新更新