While I was trying to generate code to bind C++ to a scripting language, I discovered that C++ templates, in their current modern form, are not as scary as I used to think they were. Here are a few useful things that I discovered.
First, in toy form, the problem that I was trying to solve. Suppose I have a free function that I want to bind to a virtual machine.
1
2
3
|
double convertToDegrees( double rad ) {
return rad * ( 180.0 / 3.14159265359 );
}
|
Communication with the virtual machine is carried out with these template functions.
1
2
3
4
5
6
7
|
// When called, returns the argument at position index within the argument list.
template< typename T>
T GetArgument( int index );
// when called, returns the value to the function in the virtual machine.
template< typename T >
void Return( T val );
|
I could wrap convertToDegrees
in the virtual machine glue code as follows, and give it to the virtual machine’s C API.
1
2
3
4
|
void wrapper() {
double res = convertToDegrees( GetArgument<double>( 1 ) );
Return( res );
}
|
But writing this for every function will take time. Additionally, if I were to change the function definition, then I would have to alter the wrapper manually. I want to generate these bindings automatically.
Given a free function, I want to generate the appropriate calls to GetArgument and place them in the correct order in the function call. Thus, a way to index and get a function’s argument types is needed.
Function traits
Storing function traits can be done easily.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
template< typename F >
struct function_traits;
template< typename R, typename... Args >
struct function_traits< R( Args... ) > {
using return_type = R;
constexpr static const std::size_t arity = sizeof...( Args );
template< std::size_t N >
struct argument {
static_assert( N < arity, "FunctionTraits error: invalid argument count parameter" );
using type = std::tuple_element_t< N, std::tuple< Args... > >;
};
template< std::size_t N >
using argument_type = typename argument<N>::type;
};
|
The class can be used like this:
1
2
3
|
using traits = function_traits< decltype(convertToDegrees) >;
cout << is_same<double, traits::argument_type<0>>::value << endl;
cout << is_same<double, traits::return_type>::value << endl;
|
You can use the decltype
specifier to get the function’s type, or just enter it manually. For convertToDegrees
, the type would be double(double)
.
Unpacking a tuple into a function argument list
Tuples can be unpacked into a function call surprisingly easily. Use std::index_sequence to generate a non-type parameter pack of indices to index the tuple elements at compile time.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
template< typename Function, typename Tuple, std::size_t... index>
decltype( auto ) invokeHelper( Function&& f, Tuple&& tup, std::index_sequence<index...> ) {
return f( std::get<index>( std::forward<Tuple>(tup) )... );
}
template< typename Function, typename Tuple >
decltype( auto ) invokeWithTuple( Function&& f, Tuple&& tup ) {
constexpr auto Arity = function_traits< std::remove_reference_t<decltype(f)> >::arity;
return invoke_helper(
std::forward<Function>( f ),
std::forward<Tuple>( tup ),
std::make_index_sequence<Arity>{}
);
}
|
How exactly does this work? When the ellipsis operator is placed after a pattern containing a variadic parameter, the pattern is instantiated for each instance of the variadic parameter. The instances are separated by commas. In invokeHelper
, the (non-type) variadic parameter is index
.
Note the C++14-ism in the above code. In C++11, you would write function_traits< typename std::remove_reference<decltype(f)>::type >
. C++14 introduced aliasing, which allows this: template<typename T> using remove_reference_t = typename remove_reference<T>::type;
. All the type traits in type_traits
have aliases of this form in C++14.
Now this should work:
1
2
3
4
5
6
7
8
9
10
|
void say( int val, double val2 ) {
std::cout << "called with " << val << ", " << val2 << std::endl;
}
int main() {
auto tup = std::make_tuple( 5, 42.0 );
invokeWithTuple( say, tup );
return 0;
}
|
Placing GetArgument<T>
into a function argument list
We can place the calls to GetArgument<T>
in a very similar way.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
template< typename Function, std::size_t... index>
decltype( auto ) invokeHelper( Function&& f, std::index_sequence<index...> ) {
using traits = function_traits< std::remove_reference_t<decltype(f)> >;
return f( GetArgument< typename traits::template argument_type<index> >( index + 1 )... );
}
template< typename Function >
decltype( auto ) invokeWithArguments( Function&& f ) {
constexpr auto Arity = function_traits< std::remove_reference_t<decltype(f)> >::arity;
return invoke_helper(
std::forward<Function>( f ),
std::make_index_sequence<Arity>{}
);
}
|
Generate a unique wrapper class for a function
The last piece of my toy puzzle. I call invokeWithArguments
from a static method, so that I can give the static method to the virtual machine’s C API.
1
2
3
4
5
6
7
8
9
|
template< typename Signature, Signature& >
struct ForeignMethodWrapper;
template< typename R, typename... Args, R( &P )( Args... ) >
struct ForeignMethodWrapper< R( Args... ), P > {
static void call() {
invokeWithArguments( P );
}
};
|
Invoking code conditionally
Actually, I’m not done yet. I still haven’t explained how I call Return<T>( T val )
only when the function’s return value is not void
. A specialized struct can be used to store the different invocations.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// to be used with std::is_void
template<bool predicate>
struct InvokeWithoutReturningIf {
template< typename Function >
static void invoke( Function&& f ) {
invokeWithArguments( std::forward< Function >( f ) );
}
};
template<>
struct InvokeWithoutReturningIf<false> {
template< typename Function >
static void invoke( Function&& f ) {
using R = function_traits< std::remove_reference_t<decltype(f)> >::return_type;
Return<R>( invokeWithArguments( std::forward<Function>(f) ) );
}
};
|
Finally, I modified ForeignMethodWrapper
to
1
2
3
4
5
6
|
template< typename R, typename... Args, R( &P )( Args... ) >
struct ForeignMethodWrapper< R( Args... ), P > {
static void call() {
InvokeWithoutReturningIf< std::is_void<R>::value >::invoke( P );
}
};
|
which completes the solution for my toy problem.