派生类的 C++ 查找表

Posted

技术标签:

【中文标题】派生类的 C++ 查找表【英文标题】:C++ Lookup table for derived classes 【发布时间】:2021-07-29 06:33:04 【问题描述】:

我有一个包装类,它通过一个公共基类的引用向量来保存一堆派生类对象。在运行时,子对象是根据用户输入创建的。

#include <iostream>
#include <vector>
#include <memory>
#include <type_traits>


class Base 
 public:
  virtual void run() = 0;
;

class Wrapper 
 public:
  std::vector<std::shared_ptr<Base>> blocks;

  template <class Child>
  auto create() -> std::shared_ptr<Child>
  
    auto block = std::make_shared < Child > ();
    blocks.emplace_back(block);
    return std::move(block);
  

  template <typename A, typename B>
  void connect(A a, B b)
  
    using connectionType = typename A::element_type::out;
    connectionType* port = new connectionType;
    a.get()->ptr_out = port;
    b.get()->ptr_in = port;
  
;

class Child_1 : public Base 
 public:
  using in = int;
  using out = float;
  out* ptr_out;
  in* ptr_in;
  void run()  std::cout<<"running child 1\n"; *ptr_out = 1.234;;
;

class Child_2 : public Base 
 public:
  using in = float;
  using out = bool;
  out* ptr_out;
  in* ptr_in;
  void run()  std::cout<<"running child 2\ngot: "<<*ptr_in; ;
;

int main () 

  Wrapper wrapper;
  /* read config file with a list of strings of which types to create */
  std::vector < std::string > userInput;
  userInput.push_back("Type 0");
  userInput.push_back("Type 1");

  for (auto input:userInput)
  
    if (input == "Type 0")
      wrapper.create < Child_1 > ();
    else if (input == "Type 1")
      wrapper.create < Child_2 > ();
    /* and so on */
  

  /* read config file with a list of pairs of which objects to connect */
  std::vector < std::pair < int, int >>connections;
  connections.push_back(std::make_pair(0, 1));

  // e.g. user wants to connect object 0 with object 1:
  for (int i = 0; i < connections.size (); i++)
  
    auto id0 = connections[i].first;    // e.g. 0
    auto id1 = connections[i].second;   //e.g. 1

    // this will not work because Base has no typename in / out:
    // wrapper.connect (wrapper.blocks[id0], wrapper.blocks[id1]);

    // workaround:
    wrapper.connect(
        std::dynamic_pointer_cast<Child_1>(wrapper.blocks[id0]),
        std::dynamic_pointer_cast<Child_2>(wrapper.blocks[id1]));
  

  wrapper.blocks[0].get()->run();
  wrapper.blocks[1].get()->run();

  return 0;

现在,我只能存储 Base 对象的向量,这些对象不能保存每个派生对象的不同输入/输出类型。 当我想连接派生对象(存储为基类对象)时,我需要将它们动态指针转换回它们的派生类。最有效的方法是什么?

我能想到几种方法 - 用 C++ 似乎都不可能(据我所知):

有某种查找表/枚举,它返回要转换到的类型;然后我可以从用户输入“Type 0”等创建一个映射到该类型并进行相应的转换。 有某种类似 lambda 的表达式,可以返回正确转换的指针类型,以便我可以调用 wrapper.connect( lambda_expression(...), lambda_expression(...) )。 蛮力:检查每个可能的用户输入组合,并使用 dynamic_pointer_cast 调用连接函数(如编码示例所示)。这很可能不适合我的实际应用程序(目前使用大约 25 个这样的类),因为它会导致大量不可维护的函数调用...... 不知何故将通用输入/输出类型提供给基类​​,但我想不出任何方法。

我真的希望我遗漏了一些明显的东西。非常感谢任何帮助。

【问题讨论】:

我自己不喜欢dynamic_cast。它不能很好地扩展。我的偏好是根据需要在基类中声明纯虚方法,然后在子类中覆盖它们以适应。 在这种情况下我将如何使用纯虚函数?我需要根据派生类类型定义从包装器调用模板化连接函数。我需要某种类型由派生类定义的虚拟成员变量。 return std::move (block); 可以简单地为return block; 【参考方案1】:

这看起来像是双重动态调度的典型案例,但是有一个可能的简化,即输出和输入类型必须匹配。因此,这是一种半访客模式。

首先,我们将输入和输出类型的概念提取到类中,以便dynamic_cast可以针对它们:

template <class In_>
struct BaseInput 
    using In = In_;
    std::shared_ptr<In> ptr_in;
;

template <class Out_>
struct BaseOutput 
    using Out = Out_;
    std::shared_ptr<Out> ptr_out;
;

注意:我已经交换了std::shared_ptrs,而不是让原始的拥有指针在野外。

从那里,我们可以在Base 中声明一个虚拟的connectTo 函数来获得第一级动态调度:

class Base 
    public:
    virtual ~Base() = default;
    virtual void run() = 0;
    virtual void connectTo(Base &other) = 0;
;

注意:我在Base 中添加了一个虚拟析构函数。 AFAICT 由于std::shared_ptr 的类型擦除,这是多余的,但是 Clang 向我发出警告,我不愿意追赶他们。

最后,第二个动态查找可以通过Base::connectTo 的覆盖来完成,我已经在一个方便的模板中考虑了这一点:

template <class In, class Out>
struct Child
: Base
, BaseInput<In>
, BaseOutput<Out> 
    void connectTo(Base &other_) override 
        // Throws std::bad_cast if other_'s input type doesn't match our output type
        auto &other = dynamic_cast<BaseInput<Out> &>(other_);
        this->ptr_out = other.ptr_in = std::make_shared<Out>();
    
;

此时,访问者模式将交换对象并从other_ 执行第二次虚拟调用以获取第二次动态调度。但是,如上所述,我们确切地知道我们要查找的类型,因此我们只需 dynamic_cast 即可找到它。

现在我们可以简单地实现Wrapper::connect


  template < typename A, typename B > void connect (A a, B b)
  
    a.get()->connectTo(*b.get());
  

...并以这种方式定义子类:

class Child_1 : public Child<int, float> 
public:
  void run()  std::cout<<"running child 1\n"; *ptr_out = 1.234;;
;

See it live on Wandbox

【讨论】:

效果很好,非常感谢。 override 是否必要,因为它是纯虚函数?或者这是一种好的做法? @Dominic 这是一个好习惯。如果没有它,它的工作方式也一样,但这可以确保该函数实际上覆盖了某些东西。

以上是关于派生类的 C++ 查找表的主要内容,如果未能解决你的问题,请参考以下文章

C++多继承

C++类的继承与派生

关于C++基类与派生类

关于C++基类、派生类的引用和指针

C++派生类用另一个派生类覆盖基类的成员?

详解C++中基类与派生类的转换以及虚基类