Assign different functions to different structs in std::vector<>

Started by
21 comments, last by coope 8 years, 6 months ago

I resorted to making a custom class for C++ callbacks.

The reason was that I felt std::function was a sort of 'god class' that can hold all the functions, including capturing lambda state. Except I didnt think I would ever need to capture lambda state. So all the std::function logic for doing heap allocations and probably some if branches here and there seemed like useless overhead to me.

What I wanted was simple: store a pointer to an object, and a function pointer to a member function. Leave object pointer nullptr if its a free function.

(for lambdas I take the pointer of the operator() of the lambda object - I dont capture the state, the user must keep ownership of the lambda state and ensure it doesnt disappear, like with any other C++ object)

Didnt turn out to be so simple.

I eventually got it working by creating some template stuff to 'wrap'/convert a member function pointer into a regular free function (with a 'this' pointer parameter). I didnt test this with virtual functions, but it compiles fine and works with regular, member, and existing lambdas.

(Its split to 3 classes to separately handle a return type vs no return type)


namespace wle
{
	namespace data
	{
		//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;
			}
		};
	}
}

o3o

Advertisement

There are already a number of implementations for C++ delegates out there that have been more heavily tested and scrutinized, so it's probably worth giving one of them a try first before rolling your own. This is exactly the kind of non-trivial code where subtle bugs love to breed ;)

I'd tend to agree with the very first reply suggesting to use std::function<>. This avoids boxing you in if you decide the function calls should be defined as methods of a class instead. Function pointers are pretty simple to do though so they are worth some consideration as to whether you'll want to move things around or provide additional types of calls that can be made.

Anything beyond the following seems like you'd be taking on learning complicated syntax for the same semantics. Although I did see a suggestion about Lambdas which would be used with the function pointers and would be able to call object methods. A wrapper function for a method in other words.

#include <functional>
 
struct A
{
 std::function<void(arguments)> func;
}
 
or
 
struct B
{
 void (*func)(arguments);
}

std::function is simple and easily understood, so I'd use it.

This topic is closed to new replies.

Advertisement