1

I have following Event class, inside of which there is wrapper to register some callable (lambda, std::function, etc) as callback:

#include <boost/type_traits/function_traits.hpp>

template< typename ...Args >
class Callback
{
public:
    virtual void OnEvent( const Args&... args ) = 0;
    virtual void OnEvent( const EmitContext&, const Args&... args )
   {
      OnEvent( args... );
   }
};

template< typename ...Args >
class Event
{
   template< typename Callable >
   class CallbackImpl: public Callback< Args... >
   {
   public:
       CallbackImpl( Callable&& func ) : mFunc( func )
       {
       }

       void OnEvent( const Args&... args ) override
       {
           Call( mFunc, args... );
       }

       void OnEvent( const EmitContext& emit_context, const Args&... args ) override
       {
          if constexpr( boost::function_traits< std::remove_pointer < Function > >::arity && std::is_same_v< boost::function_traits< std::remove_pointer< Function > >::arg1_type, EmitContext > )
             Call( mFunc, emit_context, args... );
          else
             OnEvent( args... );
       }
   private:
       using Function = std::remove_reference_t<Callable >;
       Function mFunc;
   };

   template < typename Callable >
   void RegisterCallbackFunction( Callable&& func )
   {
      mCallback = std::make_shared< CallbackImpl< Callable > >( std::forward< Callable >( func ) );
   }

   void Emit( const Args...& args )
   {
       EmitContext ctx;
       if( mCallback )
          mCallback->OnEvent( ctx, args... );
   }

   private:
      std::shared_ptr< Callback< Args... > > mCallback;
};

Callable can accept N first arguments from Args, where 0 <= N <= sizeof( Args... ), Call function deducts appropriate invoke.

Now I need to add variant, where Callable can accept either N arguments as before or some EmitContext structure and N arguments after it.

I'm trying to get arity and 1st argument type (if there is) using boost::function traits, but I get compilation errors:

error: implicit instantiation of undefined template 'boost::detail::function_traits_helper<(lambda at collection_event_manager.cpp:71:57) *>'
  public boost::detail::function_traits_helper<typename boost::add_pointer<Function>::type>

error: incomplete definition of type 'boost::function_traits<(lambda at collection_event_manager.cpp:71:57)>'

What am I doing wrong? I've also tried to use it without std::remove_pointer and with std::remove_pointer_t with no avail.

sehe
  • 374,641
  • 47
  • 450
  • 633
Crazy Sage
  • 414
  • 2
  • 14

1 Answers1

0

From the docs:

function_traits is intended to introspect only C++ functions of the form R (), R( A1 ), R ( A1, ... etc. )

You want to "register some callable (lambda, std::function, etc) as callback". Lambdas are not guaranteed to have implicit conversions to C++ function pointers (though they might), std::function is certainly never compatible.

What I'd usually do instead of this hard-wrangling of signatures is more like duck-typing:

if constexpr(std::is_invocable_v<Callable, Args...>)
    Call(mFunc, emit_context, args...);
else
    OnEvent(args...);

This just asks the compiler whether it call with emit_context compiles. I know that this is slightly different, and it may not work as expected if your callable is e.g. [](auto&&...){}, but then again it might be just what you wanted.

Here's a simplified take that also removes the need for the template method:

Live On Coliru

#include <functional>
#include <iostream>
#include <memory>
#include <type_traits>

struct EmitContext {
    int some_value;
};

template <typename... Args> class Event {
  public:
    template <typename F> void RegisterCallbackFunction(F&& func) {
        impl_ = std::make_shared<Wrap<std::decay_t<F>>>(std::forward<F>(func));
    }

    void Emit(const Args&... args) const {
        EmitContext ctx{123};
        if (impl_)
            impl_->call(ctx, args...);
    }

  private:
    struct IWrap {
        virtual void call(const EmitContext&, const Args&... args) = 0;
    };

    template <typename F> struct Wrap : IWrap {
        Wrap(F func) : f_(std::move(func)) {}

        void call(const EmitContext& emit_context, const Args&... args) override {
            if constexpr (std::is_invocable_v<F, EmitContext const&, Args...>)
                std::invoke(f_, emit_context, args...);
            else
                std::invoke(f_, args...);
        }

        F f_;
    };

    std::shared_ptr<IWrap> impl_;
};

#include <iomanip>
int main() {
    Event<int, std::string_view> e;

    auto simple = [](int i, std::string_view s)
    {
        std::cout << "Simple i:" << i << " s:" << std::quoted(s) << "\n";
    };

    auto with_ctx = [](EmitContext const& ctx, int i, std::string_view s) {
        std::cout << "With context{" << ctx.some_value << "}: i:" << i
                  << " s:" << std::quoted(s) << "\n";
    };

    e.RegisterCallbackFunction(simple);
    e.Emit(42, "doodle");

    e.RegisterCallbackFunction(with_ctx);
    e.Emit(99, "labra");

}

Prints

g++ -std=c++17 -O2 -Wall -pedantic -pthread main.cpp && ./a.out
Simple i:42 s:"doodle"
With context{123}: i:99 s:"labra"
sehe
  • 374,641
  • 47
  • 450
  • 633
  • If I would have only (Args...) and (EmitContext, Args...) possible signatures, I would have done it that way already) But callable can accept any number of first Args, from zero to all of them and I have machinery to deduct appropriate call, but it lives in different library and knows nothing of EmitContext and I don't want to duplicate it. – Crazy Sage Jul 15 '22 at 03:44
  • It's hard to answer questions about invisible code. I addressed the question as shown. In your case I would reconsider the design (either mandating C++ function types or perhaps making the choices explicit). See also relevant https://stackoverflow.com/a/52557033/85371 – sehe Jul 15 '22 at 07:11