首页 > 解决方案 > 如何在 C++14 中定义下面描述的概念?

问题描述

我必须为自动化引擎开发一个 C++ 框架,它能够连续运行任意用户操作(具有抽象方法 Execute() 的 C++ 类)。除了通常的输入和输出参数外,这些动作还可以访问一个通用的数据结构,该结构用作所有动作的全局环境或上下文。我的问题是关于这个数据结构。

上下文扮演一个公共存储的角色,任何动作都可以将一段数据放入其中,稍后其他一些动作可以获取并使用这些数据。因此,这个上下文应该表现为不同类型的命名元素的容器,在设计时是未知的。元素可以是简单的整数、字符串或指向类的指针。它们的确切类型在运行之前是未知的。当然,当上下文被销毁时,它应该销毁它的所有元素。

有关 std::any 的建议很有用,但不能解决问题。std::any 提供了一种在内部存储数据的便捷方式,但我不希望 std::any 在接口中。也就是说,我希望一个操作可以通过调用将其数据放入存储中

MyType* pObj1 = new MyType(...);    // MyType is the user's class
pContext->addData("UniqueName", pObj1);

稍后的其他操作可以通过调用来访问这些数据

MyType* pObj1 = pContext->getData("UniqueName");

有没有办法在现代 C++ 中实现这样的概念?

标签: c++design-patternsvisual-c++c++14

解决方案


所以,在游戏开发中,这个概念通常被称为“黑板”(我确信其他域使用相同的模式——可能使用不同的名称)。

本质上,这只是一个简单的键值映射。

很多游戏要做的就是拥有template <typename... Types> class Blackboard。黑板在内部存储一组地图,每种类型一个将存储在黑板的给定模板实例中。这样,库的作者并不知道所有可能的存储类,但用户知道并使用他们需要的所有类型定义了黑板,并且您避免了对支持类型的动态分配的需要。

根据您的问题,我假设您不想要这样的实现,并且想要保留一个将存储所有可能类型的黑板实例。您还希望在销毁黑板实例时销毁值。

我选择了一个通用的黑板实现,即只能移动,并且可以存储unique_ptr. 为此,我对轮子进行了一些重新发明,并制作了一个简化版本anyHandle在我的实现中调用),可用于仅可移动类型(std::any不能存储unique_ptr,因为它要求存储类型是复制构造有能力的)。注意!这是一个幼稚而简单的实现,可以进行许多优化

这意味着黑板本身不负责释放任何内存,但如果用户希望黑板拥有变量,则可以提供unique_ptr(甚至, )。shared_ptr

C++ 黑板代码

class Blackboard
{
private:
    struct IHandle
    {
        virtual ~IHandle() = default;
    };
    template <typename T>
    struct Handle : public IHandle
    {
        Handle(T data)
            : m_Data(std::move(data))
        {
        }
        T m_Data;
        T* get()
        {
            return &m_Data;
        }
    };

public:
    template<typename T>
    void AddData(const std::string& key, T object)
    {
        m_Map[key] = std::make_unique<Handle<T>>(Handle<T>(std::move(object)));
    }

    template<typename T>
    T* GetData(const std::string& key)
    {
        auto it = m_Map.find(key);
        if (it != m_Map.end())
        {
            if (auto* handle = dynamic_cast<Handle<T>*>(it->second.get()))
            {
                return handle->get();
            }
        }
    return nullptr;
    }
private:
    std::map<std::string, std::unique_ptr<IHandle>> m_Map;
};

例子

struct MyType
{
    ~MyType() { std::cout << "~MyType()"; }
};
int main()
{
    Blackboard b;
    b.AddData("someVar", 7);
    int* someVar = b.GetData<int>("someVar");
    std::cout << "*someVar as int: " << *someVar << std::endl;
    *someVar = 88; 

    b.AddData("otherVar", std::make_unique<int>(99));
    std::cout << "*someVar after modifying, as int: " <<*(b.GetData<int>("someVar")) << std::endl;

    std::unique_ptr<int>* otherVar = b.GetData<std::unique_ptr<int>>("otherVar");
    std::cout << "*otherVar->get() as unique_ptr<int>: " << *(otherVar->get()) << std::endl;

    std::cout << "otherVar as int: " << b.GetData<int>("otherVar") << std::endl;
    //Blackboard blackboardCopy{ b }; //Does not compile
    Blackboard movedBB{ std::move(b) }; //compiles fine, b now does not contain anything
    movedBB.AddData("MyTypeVar", std::make_unique<MyType>());

    //Because "MyTypeVar" is a unique_ptr<MyType>, as soon as movedBB goes out of scope
    //MyType's dtor will be called
}

在我的电脑上输出:

*someVar as int: 7

*someVar 通过 int* 修改后,为 int: 88

*otherVar->get() as unique_ptr: 99

otherVar 作为 int: 00000000

〜我的类型()

拥有记忆

如果你想让黑板拥有自己的内存,很容易让一些模板魔术unique_ptr在存储它们之前将指针类型包装在 a 中,并在调用unique_ptr.get()时返回。getData()出于设计原因,我个人不喜欢这种方法,因为它使 Blackboard 承担了不止一项责任。

使用std::any

您可以将我糟糕的Handle实现换成std::any.To 这样做,而不是存储unique_ptr<IHandle>在地图中,只需存储std::any. 也交换dynamic_castfor std::any_cast。这样做的唯一缺点是您不能再将仅移动类型存储在黑板上,但另一方面,您可以自由地复制黑板。您还可以获得标准库为std::any.


推荐阅读