Fun Challenge: Implement Polymorphic Callback

Started by
3 comments, last by Waterlimon 8 years, 3 months ago

Hi, I am implementing Component Based Architecture in my application and have recently run into a problem. The problem could be easily solved by using std::function although as a fun project I would like to try and tackle this problem without using std::function (even if that means ending up implementing my own version of std::function).

_

Problem:

I'm at the stage where I need to allow a Component to specify a callback that will be executed upon an event.


typedef void(Component::*EventCallback) ();
typedef std::pair<Component*, EventCallback> EventDelegate;

_

The problem with the above type definition is that all components inherit from Component but will never be a direct Component. So the following line of code is not valid:


MoveComponent* mc = new MoveComponent(); // inherits from Component
EventDelegate ed(mc , &MoveComponent::moveToXY); // Not valid because it expects Component* not MoveComponent*, and same for EventCallback.

_

Any ideas on techniques and approaches that could achieve this? Some approaches I am thinking of... any obvious/other solutions I have missed?


typedef void* EventCallback; // lazy solution 

// or

// Psuedo code (I've considered using templates here but its doesn't seem to be a solution)
class EventDelegate
{
private:
	// DataType = Pointer to component type (eg MoveComponent) 
	// Func		= Pointer to component function 
	// Context	= Pointer to component

public: 
	void runCallback()
	{
		(Context->*(void(DataType::*Func)) ();
	}
};

std::unordered_map<EVENT, EventDelegate> evtRegistry;

_

Current (simplified) implementation:


typedef void(Component::*EventCallback) ();
typedef std::pair<Component*, EventCallback> EventDelegate;

class Component {
    // ...
};

class MoveComponent : public Component {
public:
    MoveComponent() {

        EventDelegate ed(this, &MoveComponent::moveToXY);
        ComponentManager::registerEvent(ed);
    }

    void moveToXY() { }
};

class ComponentManager {
public:

    static void registerEvent(EventDelegate ed) {
        evtRegistry.push_back(ed);
    }

    static void runEvent(EventDelegate ed) {
        for (int i=0; i<evtRegistry.size(); i++) {
            Component* context = evtRegistry.at(i).first;
            EventCallback ec = evtRegistry.at(i).second;
            context->*ec();
        }
    }

private:
    static std::vector <EventDelegate> evtRegistry; // vector for a simple example. Really a std::unordered_multimap<EVENT, EventDelegate>
};
Advertisement
class IDelegate {
public:
    virtual void invoke() = 0;
};

template<class T>
class Delegate : public IDelegate {
    T* instance;
    void (T::*method)();

public:
    Delegate(T* inst, void (T::*met)()) : instance(inst), method(met) {
    }

    void invoke() {
        (instance->*method)();
    }
};

// ---------------------

class Callable {
public:
    void foo() {
        std::cout << "foo called\n";
    }
};

Callable instance = new Callable();
IDelegate del = new Delegate<Callable>(instance, &Callable::foo);
del->invoke();



This uses a virtual call but is type safe.

EDIT: There is also the FastDelegate library by Don Clugston which uses some (apparently widely supported) hacks to achieve the same without virtual function calls. I don't know if the library is still recommended as it was implemented 10 years ago... I only know that the Spotify client on windows has it listed on its credits screen. smile.png

I made this when I realized C++ is lacking in that department


//Supports nonmember and member functions, lambdas (captured parameters are not stored!)
		template<typename ReturnT, typename... ParamT>
		class CallbackBase
		{
			//Member func as free function
			using MFuncT = ReturnT(*)(void*, ParamT...);
			//free function
			using FuncT = ReturnT(*)(ParamT...);
		public:
			CallbackBase()
			{ 
				reset();
			}
			template<typename ClassT, ReturnT(ClassT::*memberFunc)(ParamT...)>
			void connectMemberFunction(ClassT* object)
			{
				mObject = object;
				mMemberFunction = &memberFunctionPtrAsFreeFunction < ClassT, decltype(memberFunc), memberFunc, ParamT... > ;
			}
			template<typename LambdaT>
			//NOTE: Callback lifetime must not exceed that of lambda if variables are captured! Use connectFunction if lambda is noncapturing
			void connectLambda(LambdaT& lambda)
			{
				mObject = &lambda;
				mMemberFunction = &memberFunctionPtrAsFreeFunction < LambdaT, decltype(&LambdaT::operator()), &LambdaT::operator(), ParamT... > ;
			}
			void connectFunction(ReturnT(*func)(ParamT...))
			{
				mObject = nullptr;
				mFunction = func;
			}
			void reset()
			{
				mObject = nullptr;
				mFunction = nullptr;
			}
			//false for normal function, true for member function / lambda
			bool hasContext() const
			{
				return mObject != nullptr;
			}
			bool connected() const
			{
				return mFunction != nullptr;
			}
		protected:
			template<typename ClassT, typename MemberFuncT, MemberFuncT memberFunc, typename... SuppliedParamT>
			static ReturnT memberFunctionPtrAsFreeFunction(void* object, SuppliedParamT... params)
			{
				return (static_cast<ClassT*>(object)->*memberFunc)(params...);
			}
			void* mObject = nullptr;
			union
			{
				MFuncT mMemberFunction;
				FuncT mFunction;
			};
		};








		template<typename>
		class Callback;
		template<typename... ParamT>
		//Supports nonmember and member functions, lambdas (captured parameters are not stored!) [void return type]
		class Callback<void(ParamT...)> : public CallbackBase < void, ParamT... >
		{
			using CallbackT = Callback<void(ParamT...)>;
		public:
			void connectToStub()
			{
				connectFunction(&stub);
			}
			template<typename... SuppliedParamT>
			void operator()(SuppliedParamT&&... params)
			{
				assert(mFunction != nullptr);
				//Member function or lambda
				if (mObject != nullptr)
				{
					(*mMemberFunction)(mObject, std::forward<SuppliedParamT>(params)...);
				}
				//Nonmember function
				else
				{
					(*mFunction)(std::forward<SuppliedParamT>(params)...);
				}
			}
			explicit operator bool() const { return connected(); }
			bool operator==(const CallbackT& other) const { return mFunction == other.mFunction && mObject == other.mObject; }
			bool operator!=(const CallbackT& other) const { return !((*this) != other); }
		private:
			static void stub(ParamT...)
			{
			}
		};








		template<typename ReturnT, typename... ParamT>
		//Supports nonmember and member functions, lambdas (captured parameters are not stored!) [nonvoid return type]
		class Callback<ReturnT(ParamT...)> : public CallbackBase < ReturnT, ParamT... >
		{
			using CallbackT = Callback<ReturnT(ParamT...)>;
		public:
			template<ReturnT defaultValue = ReturnT()>
			void connectToStub()
			{
				connectFunction(&stub<defaultValue>);
			}
			template<typename... SuppliedParamT>
			ReturnT operator()(SuppliedParamT&&... params)
			{
				assert(mFunction != nullptr);
				//Member function or lambda
				if (mObject != nullptr)
				{
					return std::move((*mMemberFunction)(mObject, std::forward<SuppliedParamT>(params)...));
				}
				//Nonmember function
				else
				{
					return std::move((*mFunction)(std::forward<SuppliedParamT>(params)...));
				}
			}
			explicit operator bool() const { return connected(); }
			bool operator==(const CallbackT& other) const { return mFunction == other.mFunction && mObject == other.mObject; }
			bool operator!=(const CallbackT& other) const { return !((*this) == other); }
		private:
			template<ReturnT defaultValue>
			static ReturnT stub(ParamT...)
			{
				return defaultValue;
			}
		};

Its just a pointer to a function and a pointer to an object (which is null if its free function). Ive only done primitive testing on it so it might be breaking some rule somewhere, but maybe you get ideas.

o3o

I made this when I realized C++ is lacking in that department

[...]

Its just a pointer to a function and a pointer to an object (which is null if its free function). Ive only done primitive testing on it so it might be breaking some rule somewhere, but maybe you get ideas.

You are relying on vendor specific behaviour. :)

Calling conventions are up to the compiler and platform. So calling the member function as a free function with an implicit this argument might fail.

But as long as it works...

It's the same trick that the FastDelegate library uses. Only it has compile switches for a dozen compilers or so. :D


You are relying on vendor specific behaviour.
Calling conventions are up to the compiler and platform. So calling the member function as a free function with an implicit this argument might fail.

But as long as it works...
It's the same trick that the FastDelegate library uses. Only it has compile switches for a dozen compilers or so.

I dont specify the calling convention (I never convert the member function pointer to something else and only call it knowing both the class type and member func ptr type)

AFAIK the only requirement is that the member function pointer is compile time constant (as its used as template argument to 'wrap' it into normal nonmember function). Shouldnt compile otherwise...

Im not sure if the above assumption is true for something like virtual methods (it COULD be?). But yeah I dont feel confident using it for anything else than regular member functions which I can assume are known at compile time (better be, for my performances sake)

o3o

This topic is closed to new replies.

Advertisement