A C++ class generator
From time to time I have to work with classes with big interfaces, and there are some situations or patterns when one has to retype the interface multiple times. My first approach is to try to reduce such interfaces, but often such classes come from somewhere else, and thus it is not possible to change them.
Until recently, my main approach was to duplicate the code. I’ve thought multiple times about removing some boilerplate with code generators, but the effort and downsides always seemed too big. With macros, I failed to see how I could handle consistently functions with different numbers of parameters (or no parameters at all), types with spaces and commas, different exception specifiers, and so on.
But lately, I’ve refined a set of macros, that together with X-macros helps me to avoid most of the boilerplate:
#define FEK_EMPTY()
#define FEK_DEFER( id ) id FEK_EMPTY()
#define FEK_OBSTRUCT( ... ) __VA_ARGS__ FEK_DEFER( FEK_EMPTY )()
#define FEK_EXPAND( ... ) __VA_ARGS__
// use for parameters with embedded ","
#define FEK_PARAM( ... ) FEK_OBSTRUCT( FEK_EXPAND )( __VA_ARGS__ )
// use for return values with embedded ","
#define FEK_RET( ... ) __VA_ARGS__
// optional, use for readability
#define FEK_NOCV()
// helper for overloading a macro
#define FEK_GET_MACRO( _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, NAME, ... ) NAME
// for querying function signature
#define V0_OR_NOTHING( ... ) __VA_OPT__( V0 )
#define FEK_TYPE_VALUE0( T0 ) T0 V0_OR_NOTHING(T0)
#define FEK_TYPE_VALUE1( T0, T1 ) T0 V0, T1 V1
#define FEK_TYPE_VALUE2( T0, T1, T2 ) T0 V0, T1 V1, T2 V2
#define FEK_TYPE_VALUE3( T0, T1, T2, T3 ) T0 V0, T1 V1, T2 V2, T3 V3
#define FEK_TYPE_VALUE4( T0, T1, T2, T3, T4 ) T0 V0, T1 V1, T2 V2, T3 V3, T4 V4
#define FEK_TYPE_VALUE5( T0, T1, T2, T3, T4, T5 ) T0 V0, T1 V1, T2 V2, T3 V3, T4 V4, T5 V5
#define FEK_TYPE_VALUE6( T0, T1, T2, T3, T4, T5, T6 ) T0 V0, T1 V1, T2 V2, T3 V3, T4 V4, T5 V5, T6 V6
#define FEK_TYPE_VALUE7( T0, T1, T2, T3, T4, T5, T6, T7 ) T0 V0, T1 V1, T2 V2, T3 V3, T4 V4, T5 V5, T6 V6, T7 V7
#define FEK_TYPE_VALUE8( T0, T1, T2, T3, T4, T5, T6, T7, T8 ) T0 V0, T1 V1, T2 V2, T3 V3, T4 V4, T5 V5, T6 V6, T7 V7, T8 V8
#define FEK_TYPE_VALUE9( T0, T1, T2, T3, T4, T5, T6, T7, T8, T9 ) T0 V0, T1 V1, T2 V2, T3 V3, T4 V4, T5 V5, T6 V6, T7 V7, T8 V8, T9 V9
#define FEK_VALUE0( T0 ) V0_OR_NOTHING(T0)
#define FEK_VALUE1( T0, T1 ) V0, V1
#define FEK_VALUE2( T0, T1, T2 ) V0, V1, V2
#define FEK_VALUE3( T0, T1, T2, T3 ) V0, V1, V2, V3
#define FEK_VALUE4( T0, T1, T2, T3, T4 ) V0, V1, V2, V3, V4
#define FEK_VALUE5( T0, T1, T2, T3, T4, T5 ) V0, V1, V2, V3, V4, V5
#define FEK_VALUE6( T0, T1, T2, T3, T4, T5, T6 ) V0, V1, V2, V3, V4, V5, V6
#define FEK_VALUE7( T0, T1, T2, T3, T4, T5, T6, T7 ) V0, V1, V2, V3, V4, V5, V6, V7
#define FEK_VALUE8( T0, T1, T2, T3, T4, T5, T6, T7, T8 ) V0, V1, V2, V3, V4, V5, V6, V7, V8
#define FEK_VALUE9( T0, T1, T2, T3, T4, T5, T6, T7, T8, T9 ) V0, V1, V2, V3, V4, V5, V6, V7, V8, V9
#define FEK_GET_TYPE_VALUES( ... ) \
FEK_GET_MACRO( __VA_ARGS__, \
FEK_TYPE_VALUE9, FEK_TYPE_VALUE8, FEK_TYPE_VALUE7, FEK_TYPE_VALUE6, FEK_TYPE_VALUE5, FEK_TYPE_VALUE4, FEK_TYPE_VALUE3, FEK_TYPE_VALUE2, FEK_TYPE_VALUE1, FEK_TYPE_VALUE0 ) \
( __VA_ARGS__ )
#define FEK_GET_VALUES( ... ) \
FEK_GET_MACRO( __VA_ARGS__, \
FEK_VALUE9, FEK_VALUE8, FEK_VALUE7, FEK_VALUE6, FEK_VALUE5, FEK_VALUE4, FEK_VALUE3, FEK_VALUE2, FEK_VALUE1, FEK_VALUE0 ) \
__VA_ARGS__ )
#define FEK_GET_TYPES( ... ) __VA_ARGS__
It looks horrible, and I’ve already forgotten how FEK_OBSTRUCT
and FEK_DEFER
work. I should really write it down.
__VA_OPT__
, used inside V0_OR_NOTHING
is required for handling correctly nullary and unary functions, because in both cases, FEK_VALUE0
/FEK_TYPE_VALUE0
are selected from FEK_GET_TYPE_VALUES
/FEK_GET_VALUES
.
I’ve tried hard to avoid depending on it, since it requires at least C++20, but found no other portable way. In the projects where I cannot use C++20 (yet), the workaround is to handle nullary functions manually.
There is one obvious limitation, at most ten parameters are supported. If needed it is possible to add further overloads, lucky me, I never reached that limit.
Another limitation is perfect forwarding and moving values. The only available approaches that currently come to mind are to provide further macros like FEK_GET_MOVE_VALUES
and FEK_GET_FORWARD_VALUES
, or provide a FEK_GET_VALUES
that accepts a callback (an X-macro inside an X-macro). Lucky me I did not need something like that until now, I guess providing FEK_GET_MOVE_VALUES
and FEK_GET_FORWARD_VALUES
is less error-prone for the end-user.
But how do you use it?
The main idea is to:
-
create a tabular structure with the properties of the function, eventually by using
FEK_RET
,FEK_NOCV
, andFEK_PARAM
-
define callbacks macro (the X-macro) for generating the C++ source code, eventually with
FEK_GET_TYPE_VALUES
,FEK_GET_VALUES
, andFEK_GET_TYPES
For example
// create a list of function signatures
#define MEMBER_FUNCTIONS( X ) \
/* return type ,name ,qualifiers ,optional parameters */ \
X( FEK_RET(void) ,foo ,FEK_NOCV() , ) \
X( FEK_RET(int) ,bar ,const ,FEK_PARAM(std::pair<int, int>), int ) \
X( FEK_RET(std::pair<int, int>) ,baz ,noexcept ,const char* ) \
static_assert(true)
struct dummy {
#define MFUNCTION( RET, NAME, QUAL, ... ) RET NAME( FEK_GET_TYPES( __VA_ARGS__ ) ) QUAL;
MEMBER_FUNCTIONS( MFUNCTION );
#undef MFUNCTION
};
template<class... T>
void sink(const T&...) {}
#define MFUNCTION( RET, NAME, QUAL, ... ) RET dummy::NAME( FEK_GET_TYPE_VALUES( __VA_ARGS__ ) ) QUAL { sink(FEK_GET_VALUES( __VA_ARGS__ )); return RET(); }
MEMBER_FUNCTIONS( MFUNCTION );
#undef MFUNCTION
After executing the preprocessor, the source code should look like
struct dummy {
void foo();
int bar(std::pair<int, int>, int) const;
std::pair<int, int> baz(const char *) noexcept;
};
template<class... T>
void sink(const T&...) {}
void dummy::foo() {
sink();
return void();
}
int dummy::bar(std::pair<int, int> V0, int V1) const {
sink(V0, V1);
return int();
}
std::pair<int, int> dummy::baz(const char * V0) noexcept {
sink(V0);
return std::pair<int, int>();
}
It might not be a significant example, but hopefully, it helps to understand how to use all the macros.
Mock class
A common use case, for better or worse, that appears when writing tests is to create mocks.
There are classes with only pure member functions that define an API, and sometimes you want to create mocks, as the test might otherwise depend on external resources.
The usual approach is to define a subclass and implement all functions, even those that you do not need, until the code compiles.
// defined somewhere
struct api {
virtual bool foo() = 0;
virtual void bar(int, int) const = 0;
virtual std::pair<int, int> baz(const char*) noexcept = 0;
// ...
virtual ~api();
};
// takes api*, and uses only a small subset of api, for example only foo
void function_to_test(int, bool, api*);
// in the test suite
// even if unused, needs to implement bar and baz
struct mock_api : api {
bool foo() override{return {};}
void bar(int, int) const override{}
std::pair<int, int> baz(const char*) noexcept override{ return {}; }
// ...
};
void test() {
mock_api mapi;
function_to_test(1,true,&mapi);
}
Mock class with helper
If you need multiple mocks, a possible approach is to define a subclass that provides a common implementation for all member functions, and specializes only the function required for the actual test
struct common_mock_api : api {
bool foo() override{ std::terminate(); }
void bar(int, int) const override{ std::terminate(); }
std::pair<int, int> baz(const char*) noexcept override{ std::terminate(); }
// ...
};
// now it is possible to implement only the functions that are actually required
struct mock_api : api {
bool foo() override{return false;}
};
void test() {
mock_api mapi;
function_to_test(1,true, &mapi);
}
Mock class with X-Macro
A simple mock class can be implemented easily with the provided macros:
// enlist once the properties of the functions we are interested in
#define MEMBER_FUNCTIONS_OF_api( X ) \
/* return type ,name ,qualifiers ,optional parameters */ \
X( FEK_RET(bool) ,foo ,FEK_NOCV() , ) \
X( FEK_RET(void) ,bar ,const ,int, int ) \
X( FEK_RET(std::pair<int, int>) ,baz ,noexcept ,const char* ) \
static_assert(true)
struct mock_api : api {
#define MFUNCTION( RET, NAME, QUAL, ... ) RET NAME( FEK_GET_TYPE_VALUES( __VA_ARGS__ ) ) QUAL { return RET(); }
MEMBER_FUNCTIONS_OF_api( MFUNCTION );
#undef MFUNCTION
// ...
};
You do not need a helper mock class, but if it helps to simplify the code, you can create it too :
struct mock_api : api {
#define MFUNCTION( RET, NAME, QUAL, ... ) RET NAME( FEK_GET_TYPE_VALUES( __VA_ARGS__ ) ) QUAL { std::terminate(); }
MEMBER_FUNCTIONS_OF_api( MFUNCTION );
#undef MFUNCTION
// ...
};
If you are thinking that the example is too simplistic, that’s true.
But thanks to if constexpr
, it is not that difficult to cover much more use cases.
For example, you might want to return default values or throw an exception if a type is not default-constructible
struct s1 {
template <class T>
static T create_default_or_throw() {
if constexpr ( std::is_default_constructible_v<T> || std::is_void_v<T> ){
return T();
} else {
throw 42;
}
}
#define MFUNCTION( RET, NAME, QUAL, ... ) \
RET NAME( FEK_GET_TYPE_VALUE( __VA_ARGS__ ) ) QUAL override \
{ \
return create_default_or_throw<RET>(); \
}
MEMBER_FUNCTIONS( MFUNCTION );
#undef MFUNCTION
};
Another use-case is special-casing a single function, for example
struct s2 {
#define MFUNCTION( RET, NAME, QUAL, ... ) \
RET NAME( CAP_GET_TYPE_VALUE( __VA_ARGS__ ) ) QUAL override \
{ \
return [&]( auto* x ) -> RET { \
using T = std::remove_pointer_t<decltype( x )>; (void)x; \
if constexpr ( std::string_view( #NAME ) != "foo" ) { \
return T(); \
} else { \
throw 42; \
} \
}( static_cast<RET*>( nullptr ) ); \
}
MEMBER_FUNCTIONS( MFUNCTION );
#undef MFUNCTION
};
If you have something like c_string
or short_string
, then you can use a separate function and reduce the amount of code defined in the macro
struct s3 {
template <class T, fek::c_string str>
static T helper() {
if constexpr ( std::string_view(str) == "foo" ) {
throw 42;
} else {
return T();
}
}
#define MFUNCTION( RET, NAME, QUAL, ... ) \
RET NAME( CAP_GET_TYPE_VALUE( __VA_ARGS__ ) ) QUAL override{ \
return helper<RET, #NAME>(); \
}
MEMBER_FUNCTIONS( MFUNCTION );
#undef MFUNCTION
};
Decorator
Similarly to the mock class, another case I’ve encountered is classes that are decorators to other classes.
A minimal example would be the following one:
// defined somewhere
struct api {
virtual bool foo() = 0;
virtual void bar(int, int) const = 0;
virtual std::pair<int, int> baz(const char*) noexcept = 0;
// ...
virtual ~api();
};
struct api_decorator : api {
api* ptr;
api_decorator(api& v):ptr(&v){}
bool foo() override{
std::puts("foo");
return ptr->foo();
}
void bar(int i, int j) const override{
std::puts("bar");
ptr->bar(i,j);
}
std::pair<int, int> baz(const char* str) noexcept{
std::puts("baz");
return ptr->bar(str);
}
}
Decorator with X-macro
Again, given the repetitive structure of the code, it is possible to use a macro to generate the desired code.
// enlist once the properties of the functions we are interested in
#define MEMBER_FUNCTIONS_OF_api( X ) \
/* return type ,name ,qualifiers ,optional parameters */ \
X( FEK_RET(bool) ,foo ,FEK_NOCV() , ) \
X( FEK_RET(void) ,bar ,const ,int, int ) \
X( FEK_RET(std::pair<int, int>) ,baz ,noexcept ,const char* ) \
static_assert(true)
struct api_decorator : api {
api* ptr;
api_decorator(api& v):ptr(&v){}
#define MFUNCTION( RET, NAME, QUAL, ... ) \
RET NAME( FEK_GET_TYPE_VALUES( __VA_ARGS__ ) ) QUAL { \
std::puts(#NAME); \
return ptr->NAME(FEK_GET_VALUES( __VA_ARGS__ )); \
}
MEMBER_FUNCTIONS_OF_api( MFUNCTION );
#undef MFUNCTION
};
Type erasure
Last but not least, there is the case of non-owning type erasure.
Consider the following class (without virtual
functions, contrary to the other examples)
struct api {
bool foo();
void bar(int, int) const;
std::pair<int, int> baz(const char*) noexcept;
// ...
};
class api_view1 {
void ptr_;
bool (foo_fun*)();
void (bar_fun*)(int, int) const;
std::pair<int, int> (baz_fun*)(const char*) noexcept;
template <class T>
api_view1(T& t) : ptr(&t)
,foo_fun([](void* ptr){ return reinterpret_cast<T*>(ptr)->foo(); })
,bar_fun([](void* ptr, int i, int j){ reinterpret_cast<T*>(ptr)->bar(i,j); })
,baz_fun([](void* ptr, const char* str){ return reinterpret_cast<T*>(ptr)->baz(str); })
{}
bool foo(){ return this->foo_fun(); }
void bar(int i, int j){ this->bar_fun(i,j); }
std::pair<int, int> baz(const char* str) noexcept { return this->baz_fun(str); }
};
There are several duplications, even more than in the class of class hierarchies and mock classes.
For every function we want to implement, we need to
-
declare a function pointer as a member variable
-
declare a lambda where all parameters are forwarded and void* is cast back, and assign it to the function pointer
-
implement a member function that forwards all parameters to the function pointer
And all implementations follow the same structure.
Type erasure with X-macro
The same type-erased view type, but this time, with the code generated:
// enlist once the properties of the functions we are interested in
#define MEMBER_FUNCTIONS_OF_api( X ) \
/* return type ,name ,qualifiers ,optional parameters */ \
X( FEK_RET(void) ,foo ,FEK_NOCV() , ) \
X( FEK_RET(bool) ,bar ,const ,int, int ) \
X( FEK_RET(std::pair<int, int>) ,baz ,noexcept ,const char* ) \
static_assert(true)
class api_view2 {
void* ptr;
#define MFUNCTION( RET, NAME, QUAL, ... ) \
RET (*NAME##_fun)(void* __VA_OPT__(,) FEK_GET_TYPES( __VA_ARGS__ ));
MEMBER_FUNCTIONS_OF_api( MFUNCTION );
#undef MFUNCTION
template <class T>
api_view2( T& m ) noexcept
: ptr( &m )
#define MFUNCTION( RET, NAME, QUAL, ... ) \
, NAME##_fun( [](void* ptr __VA_OPT__(,) FEK_GET_TYPE_VALUES( __VA_ARGS__ )){ \
return reinterpret_cast<T*>( ptr )->NAME(FEK_GET_VALUES( __VA_ARGS__ )); \
} )
MEMBER_FUNCTIONS_OF_api( MFUNCTION );
#undef MFUNCTION
{}
#define MFUNCTION( RET, NAME, QUAL, ... ) \
RET NAME( FEK_GET_TYPE_VALUES( __VA_ARGS__ ) ) QUAL { \
return this->NAME##_fun(this->ptr __VA_OPT__(,) FEK_GET_VALUES( __VA_ARGS__ )); \
}
MEMBER_FUNCTIONS_OF_api( MFUNCTION );
#undef MFUNCTION
};
In this particular example, there is one particular limitation that I did not solve like I would have liked: function overloads.
Since it is not possible to have member variables with the same name, defining the member variables will fail if there are two functions with the same name.
Null object
The null object pattern is a method for avoiding to continually verify pointers for null
. It can be implemented in different ways; with the decorator pattern (as subclass), with type erasure, or as a proxy.
For simplicity, the current snippet assumes that all functions have no return value:
// enlist once the properties of the functions we are interested in
#define MEMBER_FUNCTIONS_OF_api( X ) \
/* return type ,name ,qualifiers ,optional parameters */ \
X( FEK_RET(void) ,foo ,FEK_NOCV() , ) \
X( FEK_RET(void) ,bar ,const ,int, int ) \
X( FEK_RET(void) ,baz ,noexcept ,const char* ) \
static_assert(true)
class null_api {
api* ptr;
null_api( api* p ) noexcept : ptr( p ){}
null_api( api& v ) noexcept : ptr( &v ){}
#define MFUNCTION( RET, NAME, QUAL, ... ) \
RET NAME( FEK_GET_TYPE_VALUES( __VA_ARGS__ ) ) QUAL override { \
if(ptr) { ptr->NAME(FEK_GET_VALUES( __VA_ARGS__ ));} \
}
MEMBER_FUNCTIONS_OF_api( MFUNCTION );
#undef MFUNCTION
};
Conclusions
In some examples, I’ve written more code with the X-macro than without, but, if I need to add other functions, there is normally no need to change anything, except one line in MEMBER_FUNCTIONS_OF_api
.
With the X-macro, it is possible to extract with a callback (and all FEK_
macros) the tokens we are interested in for implementing classes:
-
types of the parameters
-
name of the parameters
-
function signature
A much better solution would actually be to parse the interface of a class, extract from there the function signatures, and then do whatever needs to be done.
The main disadvantage is that parsing the API of a class might not be trivial. For example, in the case of class hierarchies, it might be split over multiple files.
The declaration of a class might also contain some macros, making the parsing even more complex.
It is a doable task, but
-
it complicates the build system, as one needs to execute a separate tool
-
maintaining such a parser is probably less easy to overlook than a table with the functions
-
determining the file dependencies for finding all member functions of the api of the class might not be trivial
-
to parse correctly a file, you need to support many more features than the one required for parsing the member functions of a class (for example namespaces), granted, it is possible to take some shortcuts
On the other hand, it might be easier to debug in case of errors, it should be able to handle function overloads, and should be even more flexible, although at that point you are not writing C++ anymore, but in whatever language the parser has been defined too.
While both approaches seem very tempting (implementing a repetitive pattern only once feels very often satisfactory), one should not abuse them.
Unless the class has at least ten methods, using a class generator is probably an overkill. It does not matter if the class generator is implemented with macros or with an external program.
Do you want to share your opinion? Or is there an error, some parts that are not clear enough?
You can contact me anytime.