【问题标题】:Derived class discovery at compile time编译时的派生类发现
【发布时间】:2018-09-16 13:18:53
【问题描述】:

我有一个基类,我的很多类都派生自例如:

class BaseSystem
{
public:
   virtual void doThing() = 0;
}

我希望能够标记类的所有派生类型,以便在应用程序启动时找到它们。在 c# 中,我会通过反射来查找属性或仅查找从基类派生的任何内容。

有没有类似的方法我可以在 c++ 中做到这一点,我可以用一些东西创建类并在编译时发现它们并在向量中为它们创建一个实例?

编辑: 更多关于我要解决的问题的背景信息。

我正在创建一个静态库,允许程序员实现实体组件系统模式以形成游戏引擎的基本内容。这个想法是库有一个基类,他们可以从中实现系统,系统管理器将能够发现它们,然后在游戏开始时运行它们。

【问题讨论】:

  • 所以你想要一个std::vector<Base*>,它有一个每个派生类的对象?为什么?
  • 这听起来像是一个 XY 问题。如果你有这样一个实例向量,你会用它做什么?
  • 简单的答案是“不”。没有办法在 C++ 中列出类或类成员。这基本上是反射的众多用途之一,而 C++ 没有。您必须手动列出这些类。
  • 也许std::is_base_of 可以帮助你..?
  • 根据 melpomene 的评论,这看起来像一个 XY 问题 - 你想做 X,认为 Y 会实现 X,但你不知道如何做 Y,所以问怎么做。 XY 问题让人们感到沮丧,因为 Y 通常是无法实现的,而且对于不了解 X 的人来说也毫无意义。所以尝试描述您想要解决的实际问题(即 X) - 可能有一些有用的解决方案不依赖关于解决您所询问的虚假问题 (Y)。

标签: c++ c++17


【解决方案1】:

正如其他人在 cmets 中对您的问题所指出的那样,通常不可能在编译时使用 C++ 检测特定基类的所有现有派生类。

但是,如果您只需要一种机制来避免在一个地方知道所有现有的派生类,那么您可以做一些事情,尽管不是在编译时。

基本思路

这个想法是使用静态成员变量的初始化(保证在 main 执行之前发生)在公共注册表中注册派生类。

这样的注册表可能如下所示:

class derived_registry
{
public:
    static std::size_t number_of_instances()
    {
        return _instances.size();
    }

    static base* instance(std::size_t const index)
    {
        assert(index < _instances.size());
        return _instances[index].get();
    }

    template <typename T, std::enable_if_t<std::is_base_of_v<base, T>, int> = 0>
    static std::size_t register_derived_class(std::unique_ptr<T> instance)
    {
        auto const index = _instances.size();
        _instances.emplace_back(std::move(instance));
        return index;
    }

private:
    static std::vector<std::unique_ptr<base>> _instances;
};

inline std::vector<std::unique_ptr<base>> derived_registry::_instances;

base 的任何派生类现在都必须通过调用derived_registry::register_derived_class 来注册自己,以初始化静态成员变量,例如像这样:

// in derived1.h
class derived1 : public base 
{
public:
    derived1();

    void do_something() override;

private:
   static std::size_t _index;
};

// in derived1.cpp
std::size_t derived1::_index = derived_registry::register_derived_class(std::make_unique<derived1>());

derived1::derived1()
    : base{} 
{
}

void derived1::do_something()
{
    std::cout << "derived 1\n";
}

这也说明了为什么将derived_registry::_instances 向量定义为inline 变量很重要:我们需要确保derived_registry::register_derived_class 仅在derived_registry::_instances 已经初始化之后才被调用。最简单的方法是使用这样一个事实,即当在一个翻译单元中定义了多个具有静态存储持续时间的变量时,保证它们按照定义的顺序进行初始化。由于我们在头文件中定义了derived_registry::_instances,我们保证derived_registry::_instancesderived1::_index之前被初始化,因此在derived_registry::register_derived_class的调用之前。

您可以在 wandbox 上看到这种方法的实际应用。

让它万无一失

虽然上面的实现是可行的,但它相当麻烦,并且仍然有可能有人添加了一个新的派生类但忘记注册它。

为了简化注册部分,您可以使用this question 的答案中描述的 CRTP 模式,StoryTeller 在您的问题的评论中链接。

虽然这可以显着简化注册,但注册仍然只有在每个派生类都从 CRTP 基类继承或实现注册本身时才能正常工作,这很容易忘记。为了确保除了从 CRTP 基类继承之外别无选择,您可以另外将 base 的构造函数设为私有,并使 CRTP 基类成为唯一的朋友。那么在没有注册新的派生类的情况下就不能不小心直接从base继承。

【讨论】:

  • 我不确定这实际上是否可移植。静态成员不是“保证在main 之前初始化”。相反,松散地说,它们只保证在它出现的 TU 中的任何其他实体被 odr 使用之前被初始化,因此,如果您对链接的某个模块没有代码依赖关系,则没有标准要求全局初始化器被调用。它可能在实践中有效,但我认为您不能根据标准保证这一点。见eel.is/c++draft/basic.start.dynamic#4
  • @KerrekSB 你说得对,我记错了 [basic.start.static] eel.is/c++draft/basic.start#static-1 中给出的静态初始化保证,用于对具有静态存储持续时间的各种变量进行初始化。将_instances 标记为inline 的基本原理是为了防止静态初始化顺序失败。
  • @KerrekSB 但我认为这仍然可以按照标准工作,因为幸运的是我们正在处理自己定义虚拟成员函数覆盖的派生类,如果我正确阅读 eel.is/c++draft/basic.def.odr#7 已经仅通过在该 TU 中定义来使用 ODR。因此,TU 中有另一个实体定义了静态成员变量 _index,它是 ODR 使用的,因此 _index 被初始化。
  • 非常感谢您的详细解释。这解决了我的问题:)
猜你喜欢
  • 1970-01-01
  • 2023-04-04
  • 2019-02-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-08
  • 1970-01-01
  • 2013-12-09
相关资源
最近更新 更多