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

时间:2013-10-25 19:21:34

标签: c++ design-patterns c++11 rtti

假设我有一系列类都可以实现相同的接口,可能用于调度:

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

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

现在假设我有一个Scheduler类,您可以要求为给定的描述符启动一个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。也许每种计划的项目类型都有自己的地图实例的方式?设计模式,还是......?

5 个答案:

答案 0 :(得分:5)

函数或类静态成员的地址保证是唯一的(只要<可以看到),因此您可以使用这样的地址作为密钥。

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[]来避免双重查找并简化函数体。

答案 1 :(得分:2)

这是一种方式。

IScheduler添加纯虚拟方法:

virtual const char *getId() const =0;

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

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

此外,要在编译时确实具有确切类型的模板中使用,在相同文件中定义静态方法,您可以在不使用类实例(AKA static polymorphism)的情况下使用它:

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

然后将其用作缓存映射键。 __FILE__符合C ++标准,所以这也是可移植的。

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

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


如果您有类似

的内容,请注意类层次结构
  • A继承IScheduler,必须覆盖getId()
  • A2继承A,编译器不会抱怨遗忘getId()

然后,如果你想确保不小心忘记覆盖getId(),那么你应该

  • abstract Abase继承IScheduler,未定义getId()
  • 最终A继承Abase,必须添加getId()
  • 最终A2继承Abase,除了对A的更改外,还必须添加getId()

(注意:final keyword identifier with special meaning是C ++ 11的功能,对于早期版本,请将其删除......)

答案 2 :(得分:2)

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

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;
}

如果Scheduler不是单例,您可以使用相同的技术使用中央注册表,但将Scheduler * / Descriptor对映射到unique_ptr。

答案 3 :(得分:1)

static_cast ItemType*void*并将其存储在地图中。 然后,在findOrCreate中,只需将void*static_cast恢复为ItemType*

static_cast T * - &gt; void * - &gt; T *保证让你回到原始指针。您已经将typeid(ItemType)用作密钥的一部分,因此只有在请求完全相同的类型时才能保证查找成功。所以这应该是安全的。

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

答案 4 :(得分:1)

如果您知道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将无法编译。因此,如果您需要一些真正动态的东西,您可以临时添加新的调度程序,这不会起作用。但如果这不是一个要求,那么效果很好。