Feeling bored? Do you want to refresh template metaprogramming a little and see some of the new and cool features of C++17 in action?
Look no further, here is a small toy project.
The task at hand is to determine, at compile time, the return and argument type(s) of functions and (almost) any callable type. Let us call such a type T
and split the task further into
- Determine if
T
is a callable type at all. - Extract the return type of
T.
- Extract the arguments of
T
into atuple
.
As we will see, the template “juggling” will become easier with C++17, than it was with previous standards. Throughout this post I will use this gist for the code snippets. To keep things simple,
let us from now on handle ordinary functions as callables too.
Ok, let’s get started with the first task.
Determine if a type is a callable
With the help of std::function
, it has become very simple to wrap any callable.
And with a new C++17 feature called class template argument deduction, CTAD, we no longer have to provide template arguments for this wrapping.
Finally, we use std::declval
to provide an object of type T
(which is required for std::function
) on the fly at compile time:
This simple metafunction wraps any callable into std::function
.
Unfortunately gcc did not like this one, and it took me one day to figure out that
gcc has a bug here.
Clang and MSVC are happy with this, so I had to write a not so nice macro to adjust
for different compilers:
With AsFunction
we can wrap any callable, but we get a bit cryptic compile errors if we were to pass a non-callable type.
For our task we need a predicate which takes any type T
and decides if T
is a callable or not.
With the help of the new C++17 std::void_t
it is not that hard:
If you want to know more about this metafunction pattern with std::void_t
, please watch this excellent presentation by the inventor Walter E. Brown. So what happens here?
First, the primary template is_callable
assumes that T
is not a callable.
The specialization then calls the AS_FUNCTION
macro, which will be valid only if T
is a callable.
If it is, then std::void_t
will return void
, and this specialization is chosen(by deduction rules) with std::true_type
.
If T
is not a valid callable expression, then SFINAE kicks in and the primary template is
chosen with std::false_type
.
Finally, is_callable_v
is just a helper template, where we do not have to type ::value
every time we call is_callable
. Below are examples of how simple it is to use the predicate:
Extract the return type of any callable
With the results of the previous task we can now dissect any callable into its constituent types.
We need to figure out the return type R
and the variadic argument types Args...
of a callable type T
. With just T
at our disposal, we first define the primary template:
With this template we covered T
, but we need to introduce R
and Args...
as well. Here again comes the allmighty std::function
into the picture:
This means that our callable type T
with R
and Args...
will be matched against a std::function<R(Args...)>
type. So we specialize function_components
with this matching and have now introduced the needed names R
and Args...
:
This is just the declaration though. We still need to define the “return type” for this template.
By convention, a metafunction shall only have one return type(which is usually denoted with type
), but we have R
and Args...
to return.
One way of packaging these types is with the help of a std::tuple
:
I have chosen this package structure:
R(Args...)
as the template parameter forstd::function
itself- The return type
R
- The decayed arguments
Args...
I am not so sure about this decomposition within function_components
.
It works as expected, but what do you think? How would you go about this?
Anyway, we now need to define the accessors to this tuple
which is more straight forward:
Here, get_function_component_t
is the tuple
selector with I
as index.
Here you see another C++17 feature in action: you can now use auto
for a
non-type template argument.
The other alias templates function_signature_t
, function_result_t
and function_arguments_t
select their mapped type then by the given index.
For our task at hand we need to look at function_result_t
.
This template is almost the solution to extract the return type from any callable.
All that is left, is to define a template which takes a callable and converts it into a std::function
…which is exactly what we did in the first step 😉
To ease the usage of our template, we provide a simple wrapper which will also give nice a compile error, if the type provided is not a callable:
So, the solution to our taks is now this simple template:
The template callable_result
just takes a type T
and returns the result/return type if T
is a callable. The reason why I used the term “result” instead of “return” is that std::function
(amongst other type constructors) also use “result”.
As an example for the usage:
Extract the arguments of any callable into a tuple
Why should the arguments of a callable go into a tuple?
Variadic arguments cannot be stored as they are. They need to have some kind of variadic container and std::tuple
seems like an idiomatic choice.
At least that’s what my impression is when looking at others in the C++ community.
This task can now be solved exactly like the previous one. A simple template
will extract the arguments:
All the hard work was already done while we dissected the std::function
type earlier.
There, the variadic arguments were already stored into a tuple.
As an example for the usage:
Conclusion
Maybe you agree by now that juggling with templates has become less cryptic with C++17.
It has been an interesting endeavor to play around with callables traits. How is this useful? See an upcoming post about memoization, where all this stuff will be needed.
You can play with the code on godbolt.
Please drop a comment if you think this information was useful or something is wrong or could be done better. Thanks!