3

I'd like to have some sort of structure/type/map which can contain std::function specialisations (to contain my callbacks) which have the type known at compile time, without having to do any sort of virtual inheritance or making all my types inherit from a base type.

e.g. I'd like GenericFunction in here to hold the std::function without erasing the known type, so I can get the type later on during the callback()

#include <unordered_map>
#include <string>
#include <functional>
#include <iostream>

// Example struct that I want
template<class CallbackDatatype>
struct GenericFunction{
    GenericFunction(const std::function<void(const CallbackDatatype&)> callback) 
     : m_callback(callback)

    std::function<void(const CallbackDatatype&)> m_callback;
    static T datatypePlaceholder;
}

class Test
{
public:
Test() = default;

template <class CallbackDatatype>
void attachCallback(const std::string& key, const std::function<void(const CallbackDatatype&)>& callbackFn)
{
    // How do I implement GenericFunction/a map that can hold GenericFunction??
    m_callbackMap[key] = GenericFunction<CallbackDatatype>(callbackFn)
}

// Called during runtime
void callback(const std::string& key, void* payload)
{
    const auto mapIter = m_callbackMap.find(key);
    if (mapIter != m_callbackMap.end()) {
        // I need to get the datatype of the argument specified in the std::function stored in m_callbackMap to use for deserialising my payload
        decltype(mapIter->second.datatypePlaceholder) data = m_deserialiser.deserialise<decltype(mapIter->second.datatypePlaceholder)>(payload);
        mapIter->second.m_callback(data);
    }
}

private:
Deserialiser m_deserialiser;
std::unordered_map<std::string, GenericFunction> m_callbackMap;
};

struct SomeStruct{
    int a;
    int b;
    float c;
};

int main(){

// Example int function (though I want to use more complicated structs)
const fn1 = [](const int& data) -> void {
    std::cout << data << std::endl;
}

const fn2 = [](const SomeStruct& data) -> void {
    std::cout << data.c << std::endl;
}

Test test;
test.attachCallback("Route1", fn1);
test.attachCallback("Route2", fn2);


return 0;
}

Is there any way to do this?

Thank you!

kekpirat
  • 311
  • 2
  • 10
  • `GenericFunction = std::variant, std::function>` ? Also `std::any`. I do not understand - how to get type from `void* payload`? How do you intent to call functions taking different type on the same data? – KamilCuk Jan 04 '22 at 11:15
  • 2
    You have a general design bug I believe! Even if you can store a "generic" functor in some kind of container or via a wrapper, you are not able to call them, without knowing the type of that functor. In your case, if you have stored it, what data type `CallbackDatatype` would have? – Klaus Jan 04 '22 at 11:17
  • Aside: you don't need to wrap `std::function` in another class, you could define `template using GenericFunction = std::function;` – Caleth Jan 04 '22 at 14:14
  • Related: https://stackoverflow.com/q/45715219/2610810 . If you know what the types are beforehand use `std::variant` not `std::any`. – Caleth Jan 04 '22 at 14:19

2 Answers2

3

You can sort of do what you want but not with character strings but with type tags.

Here's a proof of concept.

#include <tuple>
#include <utility>
#include <type_traits>

template <typename Tag, std::size_t I, std::size_t N, typename Tup>
struct FMapHelper {
  static constexpr std::size_t helper() {
    if constexpr (I < N) {
      if constexpr (std::is_same_v<typename std::tuple_element_t<I,Tup>::first_type, Tag>) {
        return I;
      } else {
        return FMapHelper<Tag, (I+1), N, Tup>::helper();
      }
    } else {
      return N;
    }
  }
};


template <typename... TFs>
struct FMap {
  std::tuple<TFs...> tfs;

  template <typename Tag, typename F>
  auto more(Tag, F&& f) const {
    return FMap<TFs...,std::pair<Tag,F>>{std::tuple_cat(tfs, std::make_tuple(std::make_pair<Tag,F>(Tag{},std::forward<F>(f))))};
  }

  template <typename Tag>
  auto get() const {
    static constexpr std::size_t I = FMapHelper<Tag, 0, sizeof...(TFs), std::tuple<TFs...>>::helper();
    if constexpr (I < sizeof...(TFs)) {
      return std::get<1>(std::get<I>(tfs));
    } else {
      return nullptr; // no such tag found
    }
  }
};

This can be used like this:

struct tag1 {};
struct tag2 {};

int main() {
  struct Foo {};
  // example functions
  auto const foo = [](Foo const&) { return true; };
  auto const fint = [](int i) { return i*7; };
  
  // example map where e.g. tag1 refers to the foo function
  auto const fmap = FMap<>{}.more(tag1{}, foo).more(tag2{}, fint);

  // exmaple invocation
  fmap.get<tag1>()(Foo{});
  return fmap.get<tag2>()(6);
}

Here is a live demo showing that even on only O1 the compiler manages to optimise this down entirely.

bitmask
  • 32,434
  • 14
  • 99
  • 159
1

You can store std::function<void(void*)>, something like:

class Test
{
public:
    Test() = default;

    template <class CallbackDatatype>
    void attachCallback(
        const std::string& key,
        const std::function<void(const CallbackDatatype&)>& callbackFn)
    {
        m_callbackMap[key] = [=](void* payload){
            callbackFn(m_deserialiser.deserialise<CallbackDatatype>(payload));
        };
    }

    // Called during runtime
    void callback(const std::string& key, void* payload)
    {
        const auto mapIter = m_callbackMap.find(key);
        if (mapIter != m_callbackMap.end()) {
            mapIter->second(payload);
        }
    }

private:
    Deserialiser m_deserialiser;
    std::unordered_map<std::string, std::function<void(void*)>> m_callbackMap;
};
Jarod42
  • 203,559
  • 14
  • 181
  • 302