如何在地图中存储函数指针并调用它们?

How to store function pointers in map and invoke them?

我想创建 class,它应该包含带有函数指针(订阅者)的映射。但是这些函数可以有不同的签名。我的代码看起来像这样,但它没有完成,我不确定这是否正确。有人可以帮助我如何更正附加指针以映射并在 myMainClass::start() 中调用它们吗?

myMainClass.h

#pragma once
#include "iostream";
#include "mySubscriber.h"

struct myMainClass {
    myMainClass() {}
    ~myMainClass() {}

    bool callback1(int iData) {
        std::cout << "callback 1 with iData " << iData << std::endl;
    }

    bool callback2(std::string sData) {
        std::cout << "callback 2 with sData " << sData << std::endl;
    }

    bool callback3(int iData, std::string sData) {
        std::cout << "callback 1 with iData " << iData << ", sData " << sData << std::endl;
    }

    // SHOULD BE SOMETHING LIKE THIS
    bool start() {
        mySubscriber ss;
        ss.subscribe("callback1", callback1);
        ss.subscribe("callback2", callback2);
        ss.getSubscribe("callback1")(5);
        ss.getSubscribe("callback2")("test");
    }
};

mySubscriber.h

#pragma once
#include "map";
#include "string";
#include "functional";

class mySubscriber {
    typedef std::function<void()> func;
    std::map<std::string, func*> _subscribes;

public:
    mySubscriber() : _subscribes{} {}
    ~mySubscriber() {
        _subscribes.clear();
    }

    /*
    * append or change function pointer
    */
    void subscribe(std::string fName, func* f) {
        auto find = _subscribes.find(fName);
        if (find != _subscribes.end())
        {
            find->second = f;
        }
        else
        {
            _subscribes.emplace(fName, f);
        }
    }

    /*
    * get subscribe function
    */
    func* getSubscribe(std::string fName) {
        auto find = _subscribes.find(fName);
        if (find != _subscribes.end())
        {
            return find->second;
        }
        return NULL;
    }
};

首先是一些一般提示:

  • 尽可能避免使用原始指针,即使是内部!请改用 std::unique_ptr 或 std::shared_ptr!
  • 将数据集团缩减为标准容器,通过动态数据类型(如 std::string 对其进行索引并在普通 void std::function 上下文中使用它(几乎?)总是在类型擦除和丢失相应类型的安全外部访问。事实上,这甚至与普通函数和成员方法之间的进一步区别无关。

第一种可能的解决方法:

这是一个最小的工作示例,应该可以满足您的动态需求。对我来说,它在 MS VS 2017 (C++17) 上编译和运行良好。我尽量使用你原来的结构。

#include <variant>
#include <set>
#include <string>
#include <iostream>

struct myMainClass {
  myMainClass() {}
  ~myMainClass() {}

  bool callback1(int iData) {
    std::cout << "callback 1 with iData " << iData << std::endl;
    return true;
  }

  bool callback2(std::string sData) {
    std::cout << "callback 2 with sData " << sData << std::endl;
    return true;
  }

  bool callback3(int iData, std::string sData) {
    std::cout << "callback 1 with iData " << iData << ", sData " << sData << std::endl;
    return true;
  }

  template <typename T> class CallbackBaseTmpl;
  template <typename Ret, typename ...Args> 
  class CallbackBaseTmpl<Ret(Args...)> 
  {
  public:
    using Signature = Ret(Args...);

    CallbackBaseTmpl(const std::function<Signature>& func) : m_function(func) {}
    CallbackBaseTmpl(std::function<Signature>&& func) : 
     m_function(std::move(func)) {}

    inline Ret Func(Args&&... args) { return m_function(std::forward<Args>(args)...); }

  private:
    std::function<Signature> m_function;
  };

  class Callback1Type : public CallbackBaseTmpl<bool(int)>
  {
    using CallbackBaseTmpl::CallbackBaseTmpl;
  };

  class Callback2Type : public CallbackBaseTmpl<bool(std::string)>
  {
    using CallbackBaseTmpl::CallbackBaseTmpl;
  };

  class Callback3Type : public CallbackBaseTmpl<bool(int, std::string)>
  {
    using CallbackBaseTmpl::CallbackBaseTmpl;
  };

  using CompoundCallbackType = std::variant<Callback1Type, Callback2Type, Callback3Type>;
  class CallbackHolder
  {
  public:
    CallbackHolder(const CompoundCallbackType& callbackImpl) : m_callbacksImpl(callbackImpl) {}

    inline auto getIndex() const { return m_callbacksImpl.index(); }
    inline CompoundCallbackType& getImpl() const { return m_callbacksImpl; }

  private:
    mutable CompoundCallbackType m_callbacksImpl;
  };

  class CallbacksContainer
  {
  public:

    template <typename VariantType>
    bool subscribe(const VariantType& compoundType)
    {
      return subscribe(CallbackHolder(compoundType));
    }

    bool subscribe(const CallbackHolder& cHolder)
    {
      auto res = m_containerImpl.insert(cHolder);
      return res.second;
    }

    template <typename CallbackType, typename... Args>
    auto getSubscribe(Args&&... args)
    {
      // linear search - can be optimized
      for (auto& implEntry : m_containerImpl)
      {
        bool isWanted = std::visit([&args...](auto&& arg) {
          using T = std::decay_t<decltype(arg)>;
          if constexpr (std::is_same_v<T, CallbackType>)
            return true;
          else
            return false;
          }, implEntry.getImpl());

        if (isWanted)
          return std::get<CallbackType>(implEntry.getImpl()).Func(std::forward<Args>(args)...);
      }
      throw std::logic_error("Cannot access element");
    }

  private:

    struct CustomComparer {
      bool operator() (const CallbackHolder& lhs, const CallbackHolder& rhs) const
      {
        // Each variant entry allowed only once in the set
        return lhs.getIndex() < rhs.getIndex();
      }
    };

    std::set<CallbackHolder, CustomComparer> m_containerImpl;
  };

  bool start() {
    CallbacksContainer ms;
    ms.subscribe(Callback1Type(std::bind(&myMainClass::callback1, this, std::placeholders::_1)));
    ms.subscribe(Callback2Type(std::bind(&myMainClass::callback2, this, std::placeholders::_1)));

    ms.getSubscribe<Callback1Type>(5);
    ms.getSubscribe<Callback2Type>("TEST");

    ms.subscribe(Callback3Type(std::bind(&myMainClass::callback3, this, std::placeholders::_1, std::placeholders::_2)));
    ms.getSubscribe<Callback3Type>(2, "");

    return true;
  }
};

说明:我用 std::set 替换了您的原始地图作为一种注册表容器,因此仍然不允许重复。需要通过 Wrappers 做出一些努力才能实现所需的最终访问方案。 您现在可以轻松地以动态但始终非常安全的方式更改类型所需的注册函数。您可以根据自己的目的随意扩展此方案。可能有几个部分可以优化、缩短或扩展。也许还有一个很好的方法来避免 CallbackHolder 中的这种可变。可以通过实际的 typeid 排序和根据查找进行专门化来避免集合内的(非严重的一些功能)线性搜索。

根据反馈更新:

如果需要字符串作为键并且应该给予最大的自由度,即任何回调类型都应该可以提供而不需要编译时注册,这个解决方案可能是一个替代方案:

#include <map>
#include <string>
#include <iostream>
#include <functional>
#include <memory>

struct myMainClass {
  myMainClass() {}
  ~myMainClass() {}

  bool callback1(int iData) {
    std::cout << "callback 1 with iData " << iData << std::endl;
    return true;
  }

  bool callback2(std::string sData) {
    std::cout << "callback 2 with sData " << sData << std::endl;
    return true;
  }

  bool callback3(int iData, std::string sData) {
    std::cout << "callback 1 with iData " << iData << ", sData " << sData << std::endl;
    return true;
  }

  class ICallback 
  { 
  public: 
    virtual ~ICallback() = default; 
  };

  template <typename T> class TypedCallback;
  template <typename Ret, typename ...Args>
  class TypedCallback<Ret(Args...)> : public ICallback
  {
  public:
    using Signature = Ret(Args...);

    TypedCallback(const std::function<Signature>& func) : m_function(func) {}
    TypedCallback(std::function<Signature>&& func) :
      m_function(std::move(func)) {}

    inline Ret Func(Args&&... args) { return m_function(std::forward<Args>(args)...); }

  private:
    std::function<Signature> m_function;
  };

  class CallbacksContainer
  {
  private:

    template <typename T> struct CallTraits {};
    template <typename C, typename Ret, typename... Args>
    struct CallTraits<Ret(C::*)(Args...)>
    {
      using Signature = Ret(Args...);
      using ReturnType = Ret;
    };
    template <typename C, typename Ret, typename... Args>
    struct CallTraits<Ret(C::*)(Args...) const>
    {
      using Signature = Ret(Args...);
      using ReturnType = Ret;
    };

    template <typename F>
    struct FuncTraits
    {
      using FuncClass = std::decay_t<F>;
      using OperatorSignature = decltype(&FuncClass::operator());
      using signature = typename CallTraits<OperatorSignature>::Signature;
      using returnType = typename CallTraits<OperatorSignature>::ReturnType;
    };

    template <typename Ret, typename... Args>
    struct FuncTraits<Ret(Args...)>
    {
      using Signature = Ret(Args...);
      using ReturnType = Ret;
    };
    template <typename Ret, typename... Args>
    struct FuncTraits<Ret(*)(Args...)>
    {
      using Signature = Ret(Args...);
      using ReturnType = Ret;
    };
    template <typename Ret, typename... Args>
    struct FuncTraits<Ret(&)(Args...)>
    {
      using Signature = Ret(Args...);
      using ReturnType = Ret;
    };

  public:

    template <typename T>
    bool subscribe(const std::string& key, T&& func)
    {
      auto res = m_subscriptions.try_emplace(
        key, std::make_unique<TypedCallback<typename FuncTraits<T>::signature>>(std::forward<T>(func)));
      return res.second;
    }

    template <typename Ret, typename... Args>
    auto getSubscribe(const std::string& key, Args&&... args) const
    {
      using Signature = Ret(Args...);

      const auto& entry = m_subscriptions.at(key);
      auto rp = entry.get();
      auto typedCB = dynamic_cast<TypedCallback<Signature>*>(rp);
      if (typedCB == nullptr)
      {
        // TODO: Possible further check if functor can be used due to convertible types, for instance
        // with an acyclic visitor?
        std::logic_error("Wrong callback signature provided.");
      }

      return typedCB->Func(std::forward<Args>(args)...);
    }

  private:

    std::map<std::string, std::unique_ptr<ICallback>> m_subscriptions;
  };

  bool start() {
    CallbacksContainer ms;

    // Usage with non static member methods
    ms.subscribe("callback1", [this](int x) { return callback1(x); });
    ms.subscribe("callback2", [this](std::string x) { return callback2(x); });
    ms.subscribe("callback3", [this](int x, std::string str) { return callback3(x, str); });
    
    // Usage with lambda
    ms.subscribe("callback4", [](int y) { return y != 0; });

    // Usage with std::function itself
    ms.subscribe("callback5", std::function<bool(int)>([](int y) { return y != 0; }));

    // Getters - Unfortunately, exact types are required. Maybe acyclic visitor could help here?
    ms.getSubscribe<bool>("callback1", 1);
    ms.getSubscribe<bool>("callback2", std::string("TEST"));
    ms.getSubscribe<bool>("callback3", 1, std::string("TEST"));
    ms.getSubscribe<bool>("callback4", 1);

    return true;
  }
};

优点:

  • 不需要 static/compile 时间方法签名注册 -> 无变体
  • 至少在 C++20 中,方法订阅在这里会很容易,添加了一些帮助程序特征以使这里的事情变得更容易一些
  • 只使用了一张底层地图

缺点:

  • 某些点的类型安全性较低,dynamic_cast 可能有点慢,但可以通过简单的类型索引比较在性能方面得到改进
  • getSubscribe() 方法必须小心使用。这里需要精确类型(以前是动态注册的),不幸的是它不支持常见的签名转换方式。我认为目前没有办法用 C++20 之前的特性来解决这个问题。也许一些具有通用非循环访问者模式或 SFINAE magic + visitor 的技巧可能会在这里有所帮助,但我认为这打破了到目前为止的模式。如果这是一个真正的问题,人们仍然可以使用有疑问的链式参数方案,它自己保证类型安全。

您必须以某种方式将成员函数指针转换为常规的旧函数指针,以便将它们存储在同一个容器中。我可以想出三个选项:

#include <functional>

struct Foo {
  void foo(int x, int y, int z) {}

  /*
          Putting the instance as the first parameter is crucial, because the
     first argument to a member function call is an implicit this. If instance
     is not the first parameter the compiler has to shift around the argument
     list, otherwise it's a direct forwarding call.
  */
  static void callback(void* instance, int x, int y, int z) {
    return static_cast<Foo*>(instance)->foo(x, y, z);
  }
};

int main() {
  Foo foo;
  void (*f0)(void*, int, int, int){&Foo::callback};
  /*
          Capturing lambda cannot decay to function pointer, have to use
     std::function or smth. similar
  */
  std::function<void(int, int, int)> f1{
      [&](int x, int y, int z) { return foo.foo(x, y, z); }};
  auto f2 = std::mem_fn(&Foo::foo);

  f0(&foo, 1, 2, 3);
  f1(1, 2, 3);
  f2(&foo, 1, 2, 3);
}

这是生成程序集的神栓https://godbolt.org/z/K9eM4E