简体   繁体   中英

Is there a way to support 'const reference' as a function signature parameter in my generic function router design?

I'm trying to make a function router that calls correct function from std::map<uint64_t, std::function<void(T)>> map. The problem is, it can only find certain kinds of functions with certain kind of function signatures. I want it to support all kinds of functions.

Library itself:

#ifndef ENGINE_H
#define ENGINE_H

#include <iostream>
#include <map>

class Engine
{
public:

  typedef std::uint64_t hash_t;

  /* Register function to signal router. */
  template<class T>
  void attach(hash_t hash, void(*f)(T)) {
    /* Cast function ptr to std::function. */
    auto func = static_cast<std::function<void (T)>>(f);
    signal_router<T>[hash] = func;
  }

  /* Call registerd function from signal router. */
  template<class T>
  void emit(hash_t hash, T&& param) {
    try {
      signal_router<T>[hash](param);
    } catch (std::bad_function_call&) {
      int status = -4;
      std::cerr << "Signal router: no function implemented for parameter \""
                << abi::__cxa_demangle(typeid(T).name(), nullptr, nullptr, &status) << "\" " << '\n';
    }
  }
private:
  template<typename T>
  static std::map<hash_t, std::function<void (T)>> signal_router;
};

/* We must declare static instance outside of its class, altough it's private. */
template<typename T>
typename::std::map<uint64_t, std::function<void (T)>> Engine::signal_router;

#endif /* ENGINE_H */

Usage:

#include <iostream>
#include <string>
#include <functional>
#include "engine.hpp"

void f1(int i) {
  std::cout << "Hello " << i << '\n';
}

void f2(long i) {
  std::cout << "Hello " << i << '\n';
}

void f3(std::string& i) {
  std::cout << "Hello " << i << '\n';
}

int main()
{
  Engine eng;

  eng.attach(0, f1);
  eng.emit(0, 1);

  eng.attach(1, f2);
  eng.emit(1, 10l);

  eng.attach(2, f3);
  std::string s = " world";
  eng.emit(2, s);

  return 0;
}

Outputs:

Hello 1
Hello 10
Hello world

Which is correct.

But if I change void f3(std::string& i) signature to void f3(const std::string& i) it fails. As I understand, the template function is created with const parameter but it stripped out at some point and doesn't find correct the function from function map.

If I change the function f3 parameter to const std::string& it outputs:

Signal router: no function implemented for parameter "std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >"

So the const is stripped out.

How can I support all kinds of parameters (const ref, ref, values, etc...) through my template design?

When we originally attach the function, if the function parameter is const , then it's safe to also bind a mutable version:

template<class T>
void attach(hash_t hash, void(*f)(T)) {
  /* Bind to signal rounter */
  signal_router<T>[hash] = std::function<void(T)>(f); 

  /* Bind mutable version to signal router */
  using PlainT = std::remove_reference_t<T>; 
  if(std::is_reference<T>::value && std::is_const<PlainT>::value) {
      // Bind mutable version
      using MutableT = std::remove_const_t<PlainT>&;
      signal_router<MutableT>[hash] = std::function<void(MutableT)>(f); 
  }
}

Then, we can write f3 as a const function:

void f3(std::string const& i) {
  std::cout << "Hello " << i << '\n';
}

And now, main works whether or not std::string is const.

We can re-write this using pattern matching too:

template<class T>
void attach(hash_t hash, void(*f)(T)) {
  // if it's pass by value, add multiple binds for references
  signal_router<T>[hash] = std::function<void(T)>(f); 
  signal_router<T&>[hash] = std::function<void(T&)>(f); 
  signal_router<T const&>[hash] = std::function<void(T const&)>(f); 
  signal_router<T&&>[hash] = std::function<void(T&&)>(f); 
}
template<class T>
void attach(hash_t hash, void(*f)(T&)) {
  signal_router<T&>[hash] = std::function<void(T&)>(f); 
}
template<class T>
void attach(hash_t hash, void(*f)(const T&)) {
  signal_router<T const&>[hash] = std::function<void(T const&)>(f); 
  signal_router<T&>[hash] = std::function<void(T&)>(f); 
}
template<class T>
void attach(hash_t hash, void(*f)(T&&)) {
  signal_router<T&&>[hash] = std::function<void(T&&)>(f); 
}

I'm going to assume you don't support volatile.

Here are the 5 types that can be in the signature of your function pointers:

int
int const&
int &
int const&&
int &&

In your design, you cannot pass a pure int in. So we only have to worry about it as a function pointer argument.

int can be called by any of the above.

int const& can be called by any of the above.

int const&& can be called by int&& .

int& and int&& cannot be called by anything else.

Now, if our type is movable but not-copyable, the rules change.

T can only be called by T&& .

T const& can still be called by anyone.

If our type is non-movable, then T cannot be called via a proxy wrapper without an emplace system.

At the point of call, we need to invert this. If called with T& T const& and T& . If T can be copied, also check T .

If called with T const& we only check T const& and T iff T can be copied.

If called with T&& we need to check T&& and T const&& and T const& and T iff T can be moved.

If called with T const&& we only check T const&& and T if T can be copied.

So this gives us a plan of attack.

template<class T>
void populate(has_t hash, std::function<void(T)> f) {
  signal_router<T>[hash] = std::move(f);
}

template<class T>
void attach(hash_t hash, void(*f)(T&)) {
  populate<T&>(hash, f);
}
template<class T>
void attach(hash_t hash, void(*f)(const T&)) {
  populate<T const&>(hash, f); 
  populate<T&>(hash, f); 
  populate<T&&>(hash, f); 
  populate<T const&&>(hash, f);
}
template<class T>
void attach(hash_t hash, void(*f)(T&&)) {
  populate<T&&>(hash, f);
}
template<class T>
void attach(hash_t hash, void(*f)(T const&&)) {
  populate<T&&>(hash, f);
  populate<T const&&>(hash, f);
}
template<class T>
void attach(hash_t hash, void(*f)(T)) {
  if constexpr( std::is_copy_constructible<T>{} ) {
    populate<T const&>(hash, f);
    populate<T&>(hash, f);
    populate<T const&&>(hash, f);
  }
  if constexpr( std::is_move_constructible<T>{} ) {
    populate<T&&>(hash, f);
  }
}

and emit is:

template<class T>
void emit(hash_t hash, T&& param) {
  try {
    signal_router<T&&>[hash](param);
  }

volatile support would require another pass.

This used some C++17; there are ways around the if constexpr . I'd use tag dispatching.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM