Invoking a callable in C++
Invoking a callable in C++
This is my exploration of the std::invoke
utility of C++17.
I started with something vaguely related (which I am discussing here) and ended up reading the standard library implementation of std::invoke
(and that of Google's Abseil library).
The funny thing is that in the end, I decided I did not really need any of it for my original motivation, but I did gain some knowledge along the way, so worth the time!
My original motivation
Even though there are reasons not to have container based algorithms in the C++ standard library (upcoming ranges being one more reason), in my code, I find my main use case for the algorithms is still to work on the whole container. Because of that, I was looking into ways to get whole container overloads for the algorithms. In that endeavor, I stumbled upon this solution. In the usage examples at the bottom of the code, one finds two usage syntaxes:
apply_on( bobs, for_each, std::mem_fn( &Bob::stuff) );
for_each_on( bobs, std::mem_fn( &Bob::stuff ) );
where the for_each_on
is obviously not in the std
namespace.
This is very close to what I am looking for, which is a function with the name for_each
that will simply forward its call to the std::for_each
, but will work on the whole container rather than asking for two iterators.
Something that would look like that:
for_each( bobs, &Bob::stuff );
where this for_each
is not the one in namespace std
(but shares its name with it as opposed to the for_each_on
function in the solution I found online).
In adapting/inserting this solution to my code, I realized that the custom apply_on
function I found online is pretty much a particular case of C++17's std::invoke
.
Since I am not interested in directly using the apply_on
function as shown in the examples of the online solution, I thought I should just adapt it to use std::invoke
.
Unfortunately, at work, not all the external libraries that our projects depend on have transitioned to C++17.
Therefore, to use std::invoke
, I needed a working C++11 implementation (which would of course then not be in the std
namespace, but I digress).
Turns out it is not that hard to find a C++11 implementation.
That said, before using any of them, I wanted to make sure I understand the concepts so that I can provide support if necessary. Thus, I set out to read and understand those implementations. In the process, I came up with my own, which I am discussing here. I do not pretend this implementation is superior or even on par with the others I have seen (and certainly not with the standard library implementations out there), but from the tests I have made (comparing with Clang's implementation on a C++17 compiler I have access to), it seems to be as capable (probably did not think of every conceivable test). It might, however, be very slow to compile and suboptimal.
Syntax(es) of a "call" in C++
Before I embarked on this journey, I had never needed to make function calls through function pointers or member pointers. I have had the luxury/luck of working only on newer projects that did not involve that many callbacks. Lambdas have mostly always been available to me, so I was not familiar with the various call syntaxes of function pointers, member pointers and the like. Since I could use lambdas, when I needed to call some member function on all the elements of a vector, for instance, I just created a lambda doing exactly that and never considered using a pointer to the member. Thus, my first hurdle was understanding the problem space of calling something in C++.
Although the section on the magical INVOKE
entity1 in the C++ 17 standard (which is section 23.14.3 Requirements ([func.require])) has seven bullet points, when looking at the bigger picture, I think it is a useful approximation to summarize by saying there are three call syntaxes in C++:
- function call syntax
- member function pointer call syntax
- member object pointer call syntax
where member object is roughly standardese for data member. Translated in pseudo code, the three syntaxes above look like this:
invocable( arguments ); // function syntax
object.*invocable( arguments ); // member function pointer syntax
object.*invocable; // member object pointer syntax
The first syntax can be applied to any invocable that is not a member, be it a regular function, a function pointer, a lambda or a struct
defining a call operator (operator()
).
The two others are used when dealing with pointers to member.
Whether one is dealing with a pointer to member function or to member object, the standard allows using said pointer to call "into" an object of a related type either directly, through a std::reference_wrapper
or through a pointer.
In other words, there is a clause in the standard for each of the following calls (again in pseudo code):
(object.*invocable)( arguments ) // object
(wrapper.get().*invocable)( arguments ) // std::reference_wrapper
(*pointer.*invocable)( arguments ) // pointer to object
object.*invocable // object
wrapper.get().*invocable // std::reference_wrapper
*pointer.*invocable // pointer to object
Note that the parentheses in the first three lines are necessary because the function call operator (i.e. operator()
) has lower precedence than the pointer-to-member operator (i.e. operator.*
).
If the parentheses were not there, i.e. if the first line were written object.*invocable( arguments )
instead of the current syntax, then the order of operations would be invocable( arguments )
before object.*invocable
, and that would error out: the compiler would rightfully complain that the type of invocable
is not a callable because it would not access the member before trying to call it.
Adding the general function call syntax to the list above, one gets a total of seven call syntaxes, one per bullet point of the standard.
In the end, a conforming implementation of std::invoke
must provide this single function template which will, based on the type of its parameters and arguments, use the correct call syntax for the situation.
Getting there is not as easy as it seems (underestimating implementation difficulty is a recurring theme for me...).
In all the C++11 implementations I have seen, it involves at least SFINAE and function template partial ordering.
In C++17, using constexpr if, it is possible to have a simpler implementation (see cppreference.com possible implementation), but, as mentioned, that was not a possibility for me.
Before reading on (if you are still interested), I would suggest reading up on SFINAE (specifically the std::enable_if
technique/idiom), and maybe a little on function template partial ordering.
I do not explain the former at all, while I do say a little bit on the latter as I have discovered it while understanding the implementations of invoke
and this blog serves a bit as my note-taking!
C++17's std::invoke
The naïve starting point
Considering only the function call syntax, the function template needed can be as simple as:
template< typename Invocable, typename... Args >
auto invoke( Invocable invocable, Args... args )
-> decltype( invocable( args... ) ) {
return invocable( args... );
}
Although it will work for the function call syntax, this implementation is naïve, not taking into account argument passing efficiency (perfect forwarding) or noexcept
specification.
It will also obviously fail for any other syntax in the list presented in the previous section.
We need to have other specializations or overloads which will handle the other call syntaxes.
Since it is a function template and not a class/struct template, it is not possible to partially specialize it.
I do not think it is possible to use full specialization to create the overload set needed, but I might be wrong.
That said, it is however possible to overload it and select the appropriate overload via SFINAE or function template partial ordering, which is what I have seen in most implementation, and what is explored next.
Member pointers
As written above, the invoke
function will not work for member pointers.
Different approaches can be taken to deal with this problem and allow the function to be called with other member pointers.
One way is to write an overload of the function which will not take just any invocable as an argument, but only member pointers.
This overload will still need to be a template to accommodate member pointers of any type and some mechanism is needed to insure that the template is selected only when called with a member pointer.
One way to achieve this is through function template partial ordering, which is what most implementations that I have seen have used.
Since I have been influenced by those implementations, I did the same.
There could have been alternatives, for instance SFINAE.2 That said, I went with partial ordering.
This concept relies on template parameters and function arguments being such that one function is considered more specialized than the other.
The algorithm for partial ordering is well explained in this StackOverflow answer.
As stated in the answer, a comment of the original question gives a pretty good description of the concept:
Partial ordering basically checks in the parameters of two templates, if the parameter of one is more restrictive than the corresponding parameter of the other. If you have
f(T)
andf(bar<T>)
(withT
as a template parameter), then the first overload can take all possible arguments of the second overload, but the second overload can't take all possible arguments from the first overload - only those of thebar<T>
form.
Putting aside perfect forwarding and the noexcept
specification for now (we'll come back to them in the end), an overload of the function template which uses function template partial ordering can be written like this:
template< typename MemPtr, typename Obj, typename Arg1, typename... Args >
auto invoke( MemPtr Obj::* invocable, Arg1 arg1, Args... args )
-> decltype( (arg1.*invocable)( args... ) ) {
return (arg1.*invocable)( args... );
}
The template parameters are the member pointer type (MemPtr
), the type pointed into by the member pointer (Obj
), the object type the pointer is called on (Arg1
3) and the subsequent argument types (Args...
), if any.
For plain function pointers, this deduction will fail because the first function argument will not be a match, and the original overload will be selected.
For member pointers, the substitution will succeed and this overload will be considered more specialized, and it will be selected as intended.
Note again the parentheses around the arg1.*invocable
both in the decltype
and in the template body.
As mentioned in the previous section, those are mandatory.
Although this template does work, in its current form, it will be selected whenever invoke
is called with a member pointer as its first argument, even if the object you want to invoke the pointer on (the second argument to invoke
which has type Arg1
) is unrelated to the type the pointer points into (type Obj
).
This is a problem because for arbitrary unrelated types, using the function pointer from one type on the other will fail to compile.
To prevent this overload from being selected when the types are unrelated, SFINAE can be used.
To do this, a defaulted template parameter is added after the parameter pack and defaulted to enable_if_t
4 with a predicate to filter out the cases that should not match. In this case, the predicate is std::is_base_of< Obj, Arg1 >
and the solution looks like this:
template<
typename MemPtr,
typename Obj,
typename Arg1,
typename... Args,
typename = enable_if_t< std::is_base_of< Obj, Arg1 >::value >
>
auto invoke( MemPtr Obj::* invocable, Arg1 arg1, Args... args )
-> decltype( (arg1.*invocable)( args... ) ) {
return (arg1.*invocable)( args... );
}
where we can bikeshed my formatting some other time! 🙂
With this in place, this overload will not be selected when there is no inheritance (or identity) relationship between the member pointer object type (Obj
) and the invoked-on object type (Arg1
).
Alright, this overload is a step in the right direction, but it still cannot be used with member object pointers. That is because the call syntax is wrong: there must not be an argument list after the invocable. If we want to have member object pointers working, there has to be a third overload with the appropriate call syntax. If there is a third overload, there needs to be a way to select it when needed, and one cannot rely only on the function template partial ordering, since this will distinguish between callables and member pointers, but not between different member pointers, since they have the same syntax in the function argument list. For this, we must rely once more on SFINAE and the standard library type traits, adding one more defaulted template parameter after the parameter pack for one of the overloads:
template<
typename MemPtr,
typename Obj,
typename Arg1,
typename... Args,
typename = enable_if_t< std::is_base_of< Obj, Arg1 >::value >,
typename = enable_if_t< std::is_member_function_pointer< MemPtr Obj::* >::value >
>
auto invoke( MemPtr Obj::* invocable, Arg1 arg1, Args... args )
-> decltype( (arg1.*invocable)( args... ) ) {
return (arg1.*invocable)( args... );
}
template<
typename MemPtr,
typename Obj,
typename Arg1,
typename = enable_if_t< std::is_base_of< Obj, Arg1 >::value >
>
auto invoke( MemPtr Obj::* invocable, Arg1 arg1 )
-> decltype( arg1.*invocable ) {
return arg1.*invocable;
}
It should be noted that using the predicate std::is_member_object_pointer
on the last overload instead would have also worked.
Now, the overload set of the three invoke
functions defined above will be callable on anything that has the free function syntax, the member function pointer syntax or the member object (a.k.a. data member) pointer syntax, if the latter two are used directly with an object of the appropriate type (i.e. a type the member pointer points into or a type derived from it).
This constitutes only three of the seven syntaxes mandated by the standard.
None of the overloads will work if the object to call the member pointer on (arg1
) is a std::reference_wrapper
of such an object or a pointer to such an object.
Still some distance to go.
Invoked-on object type
If you have felt like this is getting verbose already, you are not going to like the rest of this blog post. Basically, for each of the two last overloads, three variants are needed (the one already defined and two more):
- one that can be called with an object of the type the member pointer points into (or a type derived from it),
- one that can be called with a
std::reference_wrapper
to an object of the type the member pointer points into (or a type derived from it), - or one that can be called on a pointer to an object of the type the member pointer points into (or a type derived from it).
Expressed in a more concrete way, considering the invoke
overloads as defined above and using the argument types in their declarations, the previous text means that if Arg1
is of the type Obj
or a type derived from it, invoke
should be able to call the member pointer with
- an object of type
Arg1
, - an object of type
std::reference_wrapper< Arg1 >
- or an object or type
*Arg1
.
As mentioned, the first case (i.e. object) is already written. Let us tackle the last two.
std::reference_wrapper
To handle the second case (i.e. std::reference_wrapper
, one has to write a template working on member pointers which will be selected only when the second argument is a std::reference_wrapper
to an object of an appropriate type, and SFINAE away otherwise.
Again, function template partial ordering is used to distinguish between function call syntax and member pointer syntax.
The new challenge is to find a way for the overload to be selected only when the second argument's type is a std::reference_wrapper
.
This kind of requirement has been solved with enable_if_t
in the previous sections and the same technique can be applied here: add a defaulted template parameter after the parameter pack and default it to enable_if_t
with an appropriate predicate.
template<
typename MemPtr,
typename Obj,
typename Arg1,
typename... Args,
typename = enable_if_t< is_reference_wrapper< Arg1 >::value >,
typename = enable_if_t< std::is_member_function_pointer< MemPtr Obj::* >::value >
>
auto invoke( MemPtr Obj::* invocable, Arg1 arg1, Args... args )
-> decltype( (arg1.get().*invocable)( args... ) ) {
return (arg1.get().*invocable)( args... );
}
template<
typename MemPtr,
typename Obj,
typename Arg1,
typename = enable_if_t< is_reference_wrapper< Arg1 >::value >
>
auto invoke( MemPtr Obj::* invocable, Arg1 arg1 )
-> decltype( arg1.get().*invocable ) {
return arg1.get().*invocable;
}
Unfortunately, there is no type trait in the standard library to determine if a type is a std::reference_wrapper
.
Such a type trait can be written like this:
template <class T>
struct is_reference_wrapper : std::false_type {};
template <class U>
struct is_reference_wrapper< std::reference_wrapper< U > > : std::true_type {};
which would probably be put in a detail
namespace as this does not need to be used by user code.
With this type trait and the definition above, the calls where the invoked-on object type is std::reference_wrapper
work.
Pointer to object
One would think the last case is handled the same way simply by replacing the type trait used in the enable_if_t
by the std::is_pointer
type trait of the standard library, but in most implementations I have seen, it is not the case.
I believe the reason is that testing with std::is_pointer
will yield false
for smart pointers even if the invoked-on pointer syntax should actually work for them.5
One could test for every smart pointer in the standard library inside the predicate of the enable_if_t
, but that would needlessly prevent user defined smart pointers to be used, and the standard library implementer (or the one implementing invoke
) cannot reliably test for every user defined smart pointer.
Thus, the implementations usually check that they are neither in the first nor in the second cases (i.e. neither directly on an appropriately typed object nor on a std::reference_wrapper
to one such object), and direct any other invoked-on object type to the last case.
This can be done once more using defaulted template parameters after the parameter pack in combination with SFINAE, much like this:
template<
typename MemPtr,
typename Obj,
typename Arg1,
typename... Args,
typename = enable_if_t< !std::is_base_of< Obj, Arg1 >::value >,
typename = enable_if_t< !is_reference_wrapper< Arg1 >::value >,
typename = enable_if_t< std::is_member_function_pointer< MemPtr Obj::* >::value >
>
auto invoke( MemPtr Obj::* invocable, Arg1 arg1, Args... args )
-> decltype( (*arg1.*invocable)( args... ) ) {
return (*arg1.*invocable)( args... );
}
template<
typename MemPtr,
typename Obj,
typename Arg1,
typename = enable_if_t< !std::is_base_of< Obj, Arg1 >::value >,
typename = enable_if_t< !is_reference_wrapper< Arg1 >::value >
>
auto invoke( MemPtr Obj::* invocable, Arg1 arg1 )
-> decltype( *arg1.*invocable ) {
return *arg1.*invocable;
}
where the first condition of !std::is_base_of< Obj, Arg1 >::value
ensures this is not the direct object case, and the second condition is the opposite of the one used in the std::reference_wrapper
case (thus insuring it is not selected in that case).
This is the last of the syntaxes to cover, and so this is a working implementation of invoke
which covers all cases mandated by the standard.
That said, some things can be made better.
If you are interested, read on.
Perfect forwarding
In order to be more efficient and prevent argument copies, perfect forwarding should be introduced into the mix.
To get perfect forwarding, one must use universal6 forwarding references, and use std::forward
on the arguments inside the implementation.
In what follows, the function call syntax and the member function pointer syntax are explored, both with a direct object case.
All other cases (i.e. member object call syntax and other invoke-on object types) can be modified to use perfect forwarding the same way, so in the name of brevity, they are not explicitly covered here.
Adding forwarding reference to the function argument list (i.e. &&
) and using std::forward
in the implementation, the invoke
template for the two situations covered can introduce perfect forwarding by being modified like this:
template< typename Invocable, typename... Args >
auto invoke( Invocable&& invocable, Args&&... args )
-> decltype(
std::forward<Invocable>(invocable)( std::forward<Args>(args)... )
)
{
return std::forward<Invocable>(invocable)( std::forward<Args>(args)... );
}
template<
typename MemPtr,
typename Obj,
typename Arg1,
typename... Args,
typename = enable_if_t< std::is_base_of< Obj, decay_t< Arg1 > >::value >,
typename = enable_if_t< std::is_member_function_pointer< MemPtr Obj::* >::value >
>
auto invoke( MemPtr Obj::*&& invocable, Arg1&& arg1, Args&&... args )
-> decltype(
(std::forward< Arg1 >(arg1).*std::forward< MemPtr Obj::* >(invocable))
( std::forward< Args >(args)... )
)
{
return (std::forward< Arg1 >(arg1).*std::forward< MemPtr Obj::* >(invocable))
( std::forward< Args >(args)... );
}
Honestly, the main difficulty becomes formatting and indentation: I find nothing is satisfactory.
I should probably just let ClangFormat do it for me.
In any case, there are two things worth noticing.
The first is the position of the ...
when forwarding the parameter pack.
If you are familiar with calling functions parameter packs, it is not surprising, but if you've never dealt with this kind of things, it can trip you at first.
The second is the use of decay_t
7 in the std::is_base_of
SFINAE condition in the second overload.
This is now necessary because the type Arg1
can now be deduced to be a reference and the predicate will be false
in that case if you do not remove the reference modifier to the type.
Essentially:
std::is_base_of< Arg1, Arg1& >::value == false
std::is_base_of< Arg1, decay_t< Arg1& > >::value == true
Other than those two little difficulties, there is nothing very surprising about the modifications to the original function if you are already familiar with perfect forwarding. If you are not, go read up on it (I have a past blog post about rvalues and perfect forwarding).
noexcept specification
One final thing that I looked into is getting the noexcept
specifier correct using the noexcept
operator.
Here is what it looks like for the same cases perfect forwarding was explored with:
template< typename Invocable, typename... Args >
auto invoke( Invocable&& invocable, Args&&... args )
noexcept(
noexcept(
std::forward<Invocable>(invocable)( std::forward<Args>(args)... )
)
)
-> decltype(
std::forward<Invocable>(invocable)( std::forward<Args>(args)... )
)
{
return std::forward<Invocable>(invocable)( std::forward<Args>(args)... );
}
template<
typename MemPtr,
typename Obj,
typename Arg1,
typename... Args,
typename = enable_if_t< std::is_base_of< Obj, decay_t<Arg1> >::value >,
typename = enable_if_t< std::is_member_function_pointer< MemPtr Obj::* >::value >
>
auto invoke( MemPtr Obj::*&& invocable, Arg1&& arg1, Args&&... args )
noexcept(
noexcept(
(std::forward< Arg1 >(arg1).*std::forward< MemPtr Obj::* >(invocable))
( std::forward< Args >(args)... )
)
)
-> decltype(
(std::forward< Arg1 >(arg1).*std::forward< MemPtr Obj::* >(invocable))
( std::forward< Args >(args)... )
)
{
return (std::forward< Arg1 >(arg1).*std::forward< MemPtr Obj::* >(invocable))
( std::forward< Args >(args)... );
}
and now the indentation is really screwed up. Another annoying thing that the reader might notice is that you basically have to write the implementation of the function thrice (see Vittorio Romeo's lightning talk about that). Not all that DRY, but hey!
Beyond std::invoke
I am sure there are other things that could be improved in this implementation
of invoke
.
For instance, from Vittorio's talk, I realized that my implementation is not constexpr
friendly.
That said, while it might not be a conforming implementation, it is a working one, and it is a more general version of the apply_on
template in my motivating use case (which, as I said, was vaguely related).
Writing this implementation made me learn a lot.
Generic function calling in C++ is a large subject where inspiration could come from other languages as well.
For instance, at C++Now 2018, Matt Calabrese presented a library (Argot) he is working on which seeks to provide better language ergonomics for invoking things, any callable.
Already, in 2016, he was making a proposal to the standards committee about unifying std::invoke
, std::apply
, and std::visit
, and then 2017, again at C++ Now, he was giving a talk about the beginnings of a similar library (if not the same library) called Call.
In this work, he not only explores how to provide a more uniform way to invoke things, but he also explores, amongst other things, argument unpacking from tuples much like in Python.
# Function taking 4 arguments and printing them
def fn(a, b, c, d):
print( a, b, c, d )
my_list = [ 1, 2, 3, 4 ]
# Unpacking list into four arguments
fn( *my_list )
From his 2018 C++ Now talk, I gather he is not ready to submit such an addition to the language and/or the standard library at this point, but I find his ideas interesting and will try to stay informed (although I am far from that level of C++).
Anyhow, I hope this post was of some interest. As Jon Kalb would say: safe coding!
Acknowledgments
I would like to thank Seph De Busser for taking the time to read this post before I published it and reassuring me that the mistakes in there were not too bad. 🙂
Notes
[1] I think INVOKE
is not strictly the same as std::invoke
, although I find this confusing.
As far as I can tell, INVOKE
was in the standard before std::invoke
and represents the idea of calling something.
std::invoke
is just the library implementation of this idea.
I could not find an appropriate name for such an entity. ↩︎
[2] For SFINAE, it would be easy to add a third defaulted template parameter after the parameter pack in the original definition of the previous section. Something like:
template<
typename Invocable,
typename... Args,
typename = enable_if_t< !std::is_member_pointer< decay_t< Invocable > >::value >
>
auto invoke( Invocable invocable, Args... args )
-> decltype( invocable( args... ) ) {
return invocable( args... );
}
I would have put it in this overload instead of putting the opposite verification in every other overload.
If you are wondering why the decay_t
is used, see the main text. ↩︎
[3] In the following function template declaration:
template< typename MemPtr, typename Obj, typename Arg1, typename... Args >
auto invoke( MemPtr Obj::* invocable, Arg1 arg1, Args... args );
the type the pointer points into (Obj
) and the type of the object the pointer will be invoked on (Arg1
) are not necessarily the same, since a derived object could be used with invoke
.
Thus, they must be different template parameters to allow the compiler to deduce different types. ↩︎
[4] One might have noticed that I said I was limiting myself to C++11, but I use the C++14 enable_if_t
and decay_t
helpers of std::enable_if
and std::decay
.
Those helpers are so useful that I usually define and use them even in C++11.
The _v
helpers cannot be defined in C++11, but the _t
helpers work perfectly.
The two of interest in this code can be defined like this:
template< bool B, typename T = void >
using enable_if_t = typename std::enable_if< B, T >::type;
template< typename T >
using decay_t = typename std::decay_t< T >::type;
↩︎
[5] Provided you use the right semantics, e.g. you std::move
the std::unique_ptr
. ↩︎
[6] I still prefer the previous term... sigh. ↩︎
[7] See note 4. ↩︎