自注册派生 class 到 std::map 的具体实现

Self registering concrete implementations of a derived class to a std::map

我想生成一个包含参数信息的摘要 class。然后,特定参数的每个具体实现都应覆盖 getName()getDescription() 方法。

struct ParameterInterface {
  virtual std::string getName() const = 0;
  virtual std::string getDescription() const = 0;
  double getValue() { return _data; }
  std::ostream& operator<<(std::ostream& os, const ParameterInterface& pInterface)
  {
      os << pInterface._data;
      return os;
  }
protected:
  double _data;
};

struct Parameter1 : public ParameterInterface {
  Parameter1(double data) {
    this->_data = data;
  }
  virtual std::string getName() const override final { return _name; };
  virtual std::string getDescription() const override final {
    return std::string("Description of Parameter1");
  };
private:
  const std::string _name = "Parameter1";
};

到目前为止一切顺利。接下来我想要某种形式的 Parametermanager class 允许 ParameterInterface 的每个具体实现将自己注册到那个 class。例如,这就是我大致想要的:

struct ParameterManager {
  static void registerParameter(std::string name, ParameterInterface parameter) {
    _parameters.insert(std::pair<std::string, ParameterInterface>(name, parameter));
  }

  static void printAllParameters() {
    for (const auto &entry : _parameters)
      std::cout << entry.first << ", " << entry.second << std::endl;
  }
private:
  static std::map<std::string, ParameterInterface> _parameters;
};

这不会编译,因为 ParameterInterface 是抽象的,在这里使用唯一指针通过 dynamic_cast 进行向下转换是否可行?

为了实现上述目标,我的第一个想法是将 Parameter1 class 的构造函数修改为

struct Parameter1 : public ParameterInterface {
  Parameter1(double data) {
    this->_data = data;
    ParameterManager::registerParameter(_name, *this);
  }
  // ...
};

但我不确定我是否可以像那样传递 *this 指针...这里的目标是我想生成几个参数(这些参数可能会随着我的项目而增长,但可能超过 100 个)但我不想有一个类似工厂的实例化类型

class parameterFactory {
// ...
  void createParameter(std::string parameter) {
    if (parameter == "Parameter1")
      // ...
    else if (parameter == "Parameter1")
      // ...
    // many more else if statements ...
  }
// ...
};

这会导致if/else语句很长,每次添加新参数都需要修改内部结构(违反Open/Closed原则)。我想要的完全有可能通过工厂设计模式实现(我是新手),如果是这样,如果有人能向我指出这一点,我将不胜感激。

要了解我希望明智地实现什么功能,请考虑这个 main 函数:

int main() {
  Parameter1 p1(3.14);
  Parameter1 p2(2.71);
  Parameter1 p3(1.00);
  ParameterManager::printAllParameters();
  return 0;
}

目前这只适用于双打(如果代码可以编译),理想情况下它应该被模板化并适用于任何类型。欢迎任何有关如何实现此结构的想法!

完整示例可在此处找到:https://godbolt.org/z/an3n9e

首先,是的,如果要将不同派生类型的对象存储到映射中,则必须使用某种指针。 std::unique_ptr 在这里就可以了。

现在,我建议使用 std::unique_ptr,因为它们会在需要时自动负责销毁对象。但是,这意味着您不能像示例中那样进行注册。 IE。您不能让一个对象在其构造函数中注册自己,因为那样 ParameterManager 会以某种方式包含该对象的 std::unique_ptr,而该对象也会例如在堆栈上。这意味着您将获得双倍免费。

所以我不完全清楚您想如何使用这些 classes。在这种情况下,我可能会这样做:

#include <cassert>
#include <map>
#include <memory>
#include <string>
#include <utility>

class ParameterManager {
  public:
    template <class T, class... Args>
    T & createParameter (Args &&... args) {
      static_assert(std::is_base_of<ParameterInterface, T>::value,
        "T must derive from ParameterInterface");

      // Check if T already exists in map, etc.
      ...

      // Good to go, create & insert a T.
      auto obj = std::make_unique<T>(std::forward<Args>(args)...);
      std::string name = obj->getName();
      // Note that we can't do obj->getName() within the emplace call, 
      // since the std::move(obj) might be executed before the getName().
      // In which case you're calling getName() on a nullptr.
      auto result = _parameters.emplace(std::move(name), std::move(obj));

      // If there's a static method that returns the name, then the above becomes:
      auto result = _parameters.emplace(T::staticName(),
        std::make_unique<T>(std::forward<Args>(args)...));

      assert(result.second == true);
      return *(result.first->second);
    }

  private:
    std::map<std::string, std::unique_ptr<ParameterInterface>> _parameters;
};

// Example use:
ParameterManager manager;
auto & p1 = manager.createParameter<Parameter1>(3.02);

请注意,在我的示例中没有任何地方 static。 IE。 ParameterManager class 不是单例。所以你可以有多个 ParameterManager 个对象,它们都包含不同的 Parameter 个对象。当然,这意味着可能必须到处传递您的 ParameterManager 对象。

如果您确实需要某种全局管理器,那么您要么必须修改管理器代码以仅具有静态函数和成员(这将有效地使其成为单例),要么实例化一个全局静态 ParameterManager对象。

已经给出了一个很好的答案,我建议使用他的解决方案,因为你的设计似乎不适合你的问题。那么为什么要在这里提供另一个答案呢?有时,让最初的概念发挥作用以探索其弱点是件好事。尽可能接近您的原始设计,我最终得到以下结果:

// header.h
#include <iostream>
#include <string>
#include <map>
#include <memory>

struct ParameterInterface;

struct ParameterManager {

  static void registerParameter(std::string name, const ParameterInterface &parameter);
  static void printAllParameters();

private:
  std::map<std::string, std::unique_ptr<ParameterInterface>> _parameters;
};


struct ParameterInterface {
    virtual std::string getName() const = 0;
    virtual std::string getDescription() const = 0;
    virtual ParameterInterface* clone() const = 0;
    double getValue() { return _data; }

    friend
    std::ostream& operator<<(std::ostream& os, const ParameterInterface& pInterface)
    {
        os << pInterface._data;
        return os;
    }
protected:
    double _data; 
};


struct Parameter1 : public ParameterInterface {
    Parameter1(double data) {
        this->_data = data;
        ParameterManager::registerParameter(_name, *this);
    }

    Parameter1* clone() const final {return new Parameter1(*this);}
    std::string getName() const final { return _name; };
    std::string getDescription() const final {
        return std::string("Description of Parameter1");
    };
private:
    const std::string _name = "Parameter1";
};

有相应的源文件:

// source.cpp
#include "source.h"

namespace
{
    ParameterManager& getParameterManager() {
        static ParameterManager p;
        return p;
    }
}

void ParameterManager::registerParameter(std::string name, const ParameterInterface &parameter) {
    getParameterManager()._parameters[name].reset(parameter.clone());
}

void ParameterManager::printAllParameters() {
    for (const auto &entry : getParameterManager()._parameters)
      std::cout << entry.first << ", " << *(entry.second) << std::endl;
}

主体与您的示例完全相同。在您的代码中,您尝试在注册时创建一个实例 ParameterInterface。正如您所注意到的,这无法编译,如果基数不是抽象的,则将可能的切片放在一边。因此,您可以引入一个克隆函数来创建给定对象的克隆并将其存储在地图中。此外,为了避免潜在的静态初始化顺序失败,ParameterManager 的静态函数使用在单独的源文件中定义的 getParameterManager

以下是您应该考虑的一些事项:

  • 注册参数后,对其所做的更改不会传播到管理器中存储的克隆。因此,你可以简单地存储参数的值,你会省去很多麻烦。
  • 我不确定是否需要当前行为:每当您注册相同类型的参数时,它都会覆盖映射中的旧条目。在您的 main 中,只有最后一个参数会包含在地图中。为避免这种情况,将名称作为构造函数的另一个输入参数。
  • 您应该只使用关键字 virtualoverridefinal
  • 之一
  • 我的代码不完整,你应该 ParameterManager 一个合适的单例

Live demo