🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Abusing macros to magically declutter a proxy call (don't laugh - it's doable... kinda)

Started by
8 comments, last by frob 3 years ago

First off - I don't think most of what I'm asking below is possible at this point in the language. Then again, the pockets of trickery run deep when it comes to crafty programmers, so I'm hoping to be proven wrong.

Secondly, I only have one target: I want this to work on the latest version of Visual Studio with std:c++latest enabled. I don't really care if it's compatible with GCC or anything else.

Thirdly, there are actually several questions below, each of which may or may not be possible to a different degree.

Here's a short code snippet as you might see it in practice:

class IInterface {
	protected:
		// implement the getter via void*
		virtual
		void* _Impl_GetIt(bool, bool, bool) = 0;
		
	public:
		// provide a non-const getter interface
		template<typename T = void>
		T* GetIt(bool a, bool b, bool c) { return static_cast<T*>(_Impl_GetIt(a, b, c)); }
		
		// forward const version to non-const; could call _Impl_GetIt() directly here,
		// but in the long run this seems more error prone to me. But that's neither 
		// here nor there
		template<typename T = void>
		const T* GetIt(bool a, bool b, bool c) const { const_cast<IInterface*>(this)->GetIt(a, b, c); }
};

class Implementation {
	public:
		void* GetIt(bool a, bool b, bool c) { return it; }
};

class Proxy
	: public IInterface, public Implementation {
	public:
		// our interface and implementation are not evertically related, so need to
		// use a forwarding proxy call to have them communicate
		void* _Impl_GetIt(bool a, bool b, bool c) { 
			return Implementation::GetIt(a, b, c);
		}
};

As you can see - implementing basic const/non-const narrowing getters with a forwarding call bewteen a pure virtual interface and implementation requires a fantastic amount of boilerplate code (if you're asking why I'm not deriving Implementation from IInterface then that's a whole different topic; please accept for now that this is intended).

My objective is to ideally reduce the above code to something like this:

class IInterface
{
	DECL_PureBaseGetter(void, GetIt, bool, bool, bool);
};

class Implementation
{
	public:
		void* GetIt(bool a, bool b, bool c) { return it; }
};

class Proxy
	: public IInterface, public Implementation
{
	public:
		FORWARD_ToBase(GetIt, bool, bool, bool);
};

Now, I'm actually kinda close to achieving this, but there are three primary problems I don't know how to deal with. Here's what my solution looks thus far (Implementation omitted as it looks the same):


class IInterface
{
		DECL_TemplatedPureBaseGetter3(void, GetIt, bool, bool, bool);
};

class Proxy
	: public IInterface, public Implementation
{
	public:
		using _BaseClass							= Implementation;

		FORWARD_ToBaseTemplated(GetIt, bool, bool, bool);
};

Partial code for the macros in question looks like this (I'm excluding the for-each expander since it's kinda lengthy, although I can post it below since it could maybe be modified to deal with at least one of the problems (number 3)):

#define __expand_param_to_braces2(T, o, comma, index) { * ((::std::remove_reference_t<o>*)(0))} comma
#define __EXPAND_ARGS_TO_BRACES_IC(...)				DO_FOR_EACH_INDEXED_COMMA(__expand_param_to_braces2, 0, __VA_ARGS__)

#define __expand_param_to_named2(T, o, comma, index) o _arg##index comma
#define __EXPAND_ARGS_TO_NAMED_IC(...)				DO_FOR_EACH_INDEXED_COMMA(__expand_param_to_named2, 0, __VA_ARGS__)

#define __expand_param_to_forward2(T, o, comma, index) (o)_arg##index comma
#define __EXPAND_ARGS_TO_FORWARD_IC(...)			DO_FOR_EACH_INDEXED_COMMA(__expand_param_to_forward2, 0, __VA_ARGS__)

#define DECL_TemplatedPureBaseGetter3(_DefaultReturn, _Name, _Arg1, _Arg2, _Arg3)																		\
		protected:																																		\
			_PureBasemethod																																\
			_DefaultReturn*							_Impl_##_Name(_Arg1, _Arg2, _Arg3) = 0;																\
		public:																																			\
			template<typename Type = _DefaultReturn>																									\
			Type*									_Name(_Arg1 a, _Arg2 b, _Arg3 c) { return static_cast<Type*>(_Impl_##_Name(a, b, c)); }				\
			template<typename Type = _DefaultReturn>																									\
			const Type*								_Name(_Arg1 a, _Arg2 b, _Arg3 c) const	{ return const_cast<_ThisType*>(this)->_Name<Type>(); }

#define __FORWARD_Impl(_BaseClass, FunctionName, ProxyName, ...)																										\
		decltype(::std::declval<_BaseClass&>().FunctionName(__EXPAND_ARGS_TO_BRACES_IC(__VA_ARGS__)))													\
				ProxyName(__EXPAND_ARGS_TO_NAMED_IC(__VA_ARGS__)) _Override {																			\
					return _BaseClass::FunctionName(__EXPAND_ARGS_TO_FORWARD_IC(__VA_ARGS__)); }



////// HELPER MACROS BELOW ///////

#define FORWARD_ToBase(FunctionName, ...) __FORWARD_Impl(_BaseClass, FunctionName, FunctionName, __VA_ARGS__)
#define FORWARD_ToBaseEx(_BaseClass, FunctionName, ...) __FORWARD_Impl(_BaseClass, FunctionName, FunctionName, __VA_ARGS__)
#define FORWARD_ToBaseTemplated(FunctionName, ...) __FORWARD_Impl(_BaseClass, FunctionName, _Impl_##FunctionName, __VA_ARGS__)
#define FORWARD_ToBaseTemplatedEx(_BaseClass, FunctionName, ...) __FORWARD_Impl(_BaseClass, FunctionName, _Impl_##FunctionName, __VA_ARGS__)


#define FORWARD_ToBaseConst(FunctionName, ...) __FORWARD_ImplConst(_BaseClass, FunctionName, FunctionName, __VA_ARGS__)
#define FORWARD_ToBaseConstEx(_BaseClass, FunctionName, ...) __FORWARD_ImplConst(_BaseClass, FunctionName, FunctionName, __VA_ARGS__)
#define FORWARD_ToBaseConstTemplated(FunctionName, ...) __FORWARD_ImplConst(_BaseClass, FunctionName, _Impl_##FunctionName, __VA_ARGS__)
#define FORWARD_ToBaseConstTemplatedEx(_BaseClass, FunctionName, ..) __FORWARD_ImplConst(_BaseClass, FunctionName, _Impl_##FunctionName, __VA_ARGS__)

#define FORWARD_ToBase0(FunctionName) __FORWARD_Impl0(_BaseClass, FunctionName, FunctionName)
#define FORWARD_ToBase0Ex(_BaseClass, FunctionName) __FORWARD_Impl0(_BaseClass, FunctionName, FunctionName)
#define FORWARD_ToBase0Templated(FunctionName) __FORWARD_Impl0(_BaseClass, FunctionName, _Impl_##FunctionName)
#define FORWARD_ToBase0TemplatedEx(_BaseClass, FunctionName) __FORWARD_Impl0(_BaseClass, FunctionName, _Impl_##FunctionName)

#define FORWARD_ToBase0Const(FunctionName) __FORWARD_Impl0Const(_BaseClass, FunctionName, FunctionName)
#define FORWARD_ToBase0ExConst(_BaseClass, FunctionName) __FORWARD_Impl0Const(_BaseClass, FunctionName, FunctionName)
#define FORWARD_ToBase0TemplatedConst(FunctionName) __FORWARD_Impl0Const(_BaseClass, FunctionName, _Impl_##FunctionName)
#define FORWARD_ToBase0TemplatedExConst(_BaseClass, FunctionName) __FORWARD_Impl0Const(_BaseClass, FunctionName, _Impl_##FunctionName)


I wrote a small explanation of what all of this does below.

If you look to the last code block, you'll see the line ////// HELPER MACROS BELOW ///////. All of the succeeding macros need to be used for various types of functions.

The problems I'm trying to solve are:

  • The first issue has to do with const and non-const functions because I don't know how to selectively emit the trailing const modifier for the proxy call. This basically means that __FORWARD_Impl has an almost identical __FORWARD_ImplConst version, which only adds the const modifier to the end of the forwarding call.
  • I specifically used a templated getter in this example as it exemplifies another problem: the need to implement a general getter underneath the templated versions that doesn't share a signature with the templated ones. This name needs to be known by both the interface and proxy classes, which means I need to draw a distinction between a macro written for a straight up simple forwarding function and one that needs to know that the top level functions were templated. In my case I'm simply automatically tacking _Impl_ in front of the (hidden) proxy getter.
  • I can deal with the expansion of multiple arguments and auto-naming them. However, the for-each macro can't emit empty fields, which means that the expansion of the declaration needs a special case if __VA_ARGS__ is empty. Again, I purposefully used an example with multiple arguments since it shows how I need a DECL_TemplatedPureBaseGetter3 macro specifically for functions with 3 arguments. You can see where this is going. Ideally, a single macro would be able to deal with any number of arguments, including zero, but I feel like this either requires the for-each expansion to be somehow differently written or I need to be able to generalize an if statement into it, which I don't think can realistically be done.

I have a feeling something like noexcept might help with dealing with issue 2. I have no idea how to go about 1 and 3 might somehow be doable, but I'm inclined to think the answer's still no.

Some notes on the code:

Since the code can seem a bit opaque, I'll highlight some of the more relevant bits:

  • I'm using using _BaseClass = Implementation; because I need to know the actual name of the implementation class (this becomes relevant when Proxy has more than one base class).
  • The main trickery takes place in the __FORWARD_Impl() and __EXPAND_ARGS_TO_BRACES_IC() macros. The idea is to retrieve the return type of the function in the base (Implementation) class by constructing the function prototype from the arguments provided by the user. Hence FORWARD_ToBaseTemplated(GetIt, bool, bool, bool); can use the 3 bool arguments to find the appropriate GetIt function in Implementation and automatically deduce the return type to from it (this works on the same premise that you can't have multiple functions with the same signature that only differ by return type). I could also get the constness of the called function, but I don't know how to use this to automatically emit the trailing const modifier within the macro itself.
  • In order to construct the signature and retrieve the return type from be base I use the following meta-implementation: decltype(::std::declval<_BaseClass&>().FunctionName({} {} {}). However, in order to deal with variants I'm using further trickery to specialize the 3 brace pairs properly. And I mean trickery, because using raw types won't work as soon as you have to null-initialize an argument that's a non-const reference. In particular, I'm creating null pointer versions of each argument and then dereferencing them to construct the virtual arguments: see { * ((::std::remove_reference_t<o>*)(0))} where o is the type name of the argument.
  • The rest is kinda technical and verbose but not all that complicated: e.g. I expand the provided parameters to bool arg0, bool arg1 and bool arg2 in the proxy call declaration, use the same names in the forwarding call, and so forth.

Advertisement

Hmm, this could be missing the mark since I'm not a hundred percent sure I understand all the requirements. But have you considered using CRTP? I feel like this might achieve what you can without using any macros at all:

// this class is used to bind the interface to the implementations => reuse
template<typename ImplementationT>
class BaseProxy
	: public IInterface, public ImplementationT
{
public:
void* _Impl_GetIt(bool a, bool b, bool c) override { 
	return ImplementationT::GetIt(a, b, c);
}
};

// this is the specific class you create
class Proxy : public BaseProxy<Implementation>
{
	// Done!
};

It seems that you are ok with always have the same name for the method in implementation? If thats not the case then the solution doesn't work, but I do have a few similar cases myself where I use CRTP to resolve much of the code-duplication (truthfully I think half my codebase consists of CRTP at this point lol).

Juliean said:
But have you considered using CRTP?

That's literally what I'm doing in my real code ?. I just omitted a bunch of template stuff from the example.

The question is how to cut down on code bloat from the interface and the forwarding calls. Especially the const/non-const getters add up quickly and if they're templated, produce a bunch of “internal” or “impl” style proxy functions, which are frankly an eyesore. Besides, those suckers add up, so I'm spearheading an effort to hide this nitty-gritty behind “clean”(er) macros.

Ah, so you want to cut down on those methods in the CRTP-base? I see. I think I'll have to pass then - I'm not that good with macros, and I usually suck up having to write wrappers as long as I don't need to do it every time for each individual implementation tbh :D

I've been tinkering with these macros on and off for a couple of weeks and am becoming quite the enthusiast. There's a bunch of reasons on top the obvious one (clarity):

  • naming consistency is enforced automatically
  • for getters you automatically get the const and non-const versions
  • the macros force the override keyword into the forwarding call, so you never screw up the signature by accident by forgetting to type in override and something like an ampersand from one of the arguments
  • I'm finding single-line declarations and forwarding calls increasingly more convenient to work with, especially when selecting code is involved
  • a name like FORWARD_ToBase() adds readability to the code, which you can't forget to include
  • and here's the most important thing: it's way faster to type ?

Level 2: Ever thought about making a proxy class that also works for partially matching interface implementation? So a class that has at least one of the interface methods implemented and anything else is filled with some empty calls?

I'm doing something like that for default constructors in my component structure. But I'm not a hundred percent sure what you have in mind - could you elaborate?

We've implemented some kind of Mixins in C#. Mixins are code snippets that are getting merged together with a class to extend functionality over the capabilities of extension methods. To achieve that, our runtime code generator also seeds the mixed type with interfaces that are required from methods provided by code components to be able to access the super type gneerated (the majority of components and the base class members). What I do here is gathering interface methods and match them to whatever is present in the super type. Everything else will be implemented using some empty default methods.

So you don't have to fully implement an interface but still are able to define it for your type.

There is a usecase however for C++ as well. Not just to communicate with an editor interface but also pass it around to functions that require these type (or something inherited from it), but not the whole majority of the interface rather than a portion of the features.

I currently solved this for our engine('s C++ part) using a Flex data type which provides access to a type's functions and also can be extended by static extension methods and pass that Flex object into a proxy class. Would be corious if that can be solved using macros as well instead

I generally don't like them.

I understand them. I use them in places like Unreal's code. But I don't like them.

They complicate debugging, they hide important details, they confuse many tools, and more. Stepping through them or into them can be a nightmare. Macros with variable behavior based on constants are a pain — especially bad are macros that run other macros multiple times with different constants, since in the debugger you can't see what version you've actually go.

For PIMPL I'd generally perfer to see the actual code than to see a macro like that which hides the details, and makes debugging several steps harder. But if that makes your life easier and faster enough to warrant it, whatever works for you.

This topic is closed to new replies.

Advertisement