RSS | GitHub |
The signature of a function is a type consisting of the return type, the function parameter types, and possibly function qualifiers. The function signature is also known as the function type.
We demonstrate how to create a type traits that works reasonably well for most callable objects.
First we need to distinguish between a function type and a callable type.
A callable type is
a type that can be invoked as a function. This includes function pointers,
lambda expressions, and function wrappers such as
std::function
.
A function type is the type of the call operator that is invoked by the
callable type.
Example | Function Pointer | Function Type |
---|---|---|
int main() |
int(*)() |
int() |
std::fminf(x, y) |
float(*)(float, float) |
float(float, float) |
std::mutex::lock() |
void (std::mutex::*)() |
void() |
[] {} |
void ( unspecified::*)() const |
void() const |
The function type does not include class information for member functions, even
though non-static member functions have an implied object argument, also known
as the this
pointer.
Why should we care about the function type?
Suppose we have to implement our own variation of std::async()
that executes a task on a background thread. Our variation should use a pre-existing thread rather
than launching its own.
This pre-existing thread may be preoccupied with other tasks, so we enqueue our task for later execution.
We also want our variation to return an std::future
.
We must be able to store an arbitrary callable object in a queue, and this callable should return the
result in an std::future
.
This is precisely what std::packaged_task
is designed for.
This variation is really simple to implement. We assume that the queue is thread-safe.
The above does not compile because the std::packaged_task
template requires a
function type, and it cannot be deduced from the callable type F
using
class template argument deduction,
nor do we have any standard transformation traits to deduce the function type from the
callable type.
The above challenge also applies when using other type-erased function wrappers such as
std::function
or std::move_only_function
.
Our goal is to create a transformation trait that turns a callable type into a function type.
The Args...
argument types are needed for the template call operator and will
be explained later.
A callable can either be invoked directly or indirectly via its call operator.
The latter is going to complicate our transformation trait, so we start with
the easy case of directly invocable callables.
We introduce a helper template called function_type_basis
to handle the
directly invocable callable types.
We use C++20 concepts for brevity, but the function type trait can also be
implemented in C++11 using SFINAE.
In that case you should notice that that noexcept
is not part of the function
type until C++17.
We are going to need a function_constraint
concept that checks if a type is a
function type.
We can use the std::is_function
type trait that does exactly this.
If the callable type is already a function type then we are done.
If the callable type is a function pointer, then we remove the pointer to get the function type.
Similarly, if the callable type is a function reference, then we remove the reference to get the function type. We have to handle both lvalue and rvalue references.
The above matches the function reference
as in R (&)(Args...)
, but not member function reference qualification
as in R (C::*)(Args...) &
.
This is exactly what we want because we have to preserve all function qualifiers
to get the correct function type.
If the callable type is a member function pointer, then we remove the class pointer to get the function type.
We use the naive_
prefix to indicate that we are going to do something smarter
later on.
The naive implementation is to match the function type directly to deduce the
return type and the function parameter types.
The above only matches member functions without function qualifiers, so we must also match the other permutations of function qualifiers which leads to many template specializations.
The above results in 12 template specializations, and that is not even exhaustive.
We need another 12 specializations to handle the permutations involving C-style
variadic arguments such as R(Args..., ...)
.
Furthermore, if we have to support C++11/14 where noexcept
is not part of the
function type, then we need to split each specialization into two; one with and
one without noexcept
.
So we are potentially looking at 48 template specializations to deduce the
correct function type for member function pointers.
Fortunately we can use a more compact syntax instead of the naive member
function pointer matching above.
T
becomes the function type when we match the member function type with
T C::*
.
We are now able to handle all the directly invocable types with a total of just 5 simple template specializations.
Invoking function objects, including lambda expressions, results in a call to their call operator. The function type of a function object is therefore the function type of its call operator.
Consider a function object with a void()
call operator.
Deducing the function type is done in two steps
C
by taking its address, that is &C::operator()
, which gives
us a member function pointer whose type can be found using the
decltype
specifier.function_type_basis
template from the previous section already does that.The combined functionality can be implemented as:
The constraint checks if class C
has a call operator.
We hook support for function objects into our general function_type
template
with a template specialization.
The template specialization is constrained to only accept a class type. A function object must be invocable, so this may have been a more obvious constraint to use, but we would run into problems with nearly matching overloads if we try to enforce that constraint here. Instead we have made a weaker constraint by only requiring a class type.
The above implementation gives us the correct function type.
Example | Function Type |
---|---|
function_type_t<function_object_0001> |
void() |
Our function type trait still does not support function objects with a template call operators or generic lambda expressions, which for the purposes of this discussion is the same.
Consider the following function object that has a template call operator.
We want to deduce the function type given a set of arguments. This requires a template specialization that uses the address of the call operator where the argument types are explicitly added.
We have to pass the argument types to the function type trait.
Example | Function Type |
---|---|
function_type_t<accumulator, int> |
int(int) |
function_type_t<accumulator, int, int> |
int(int, int) |
Now we can finally implement our std::async
variation from the Motivation
section.
Notice that std::packaged_task
does not accept a callable with function
qualifiers, such as R(Args...) const
or R(Args...) noexcept
.
This excludes us from passing lambda expressions among others, as we have no
standard transformation traits to modify such function types, but that is a
different topic for another time.
The function type trait we have created does not work with overloaded call
operators.
We would have to disambiguate the &C::operator()
expression somehow, which
involves emulation of the overload resolution rules and that is a very deep
rabbit hole.