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++:

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) and f(bar<T>) (with T 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 the bar<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 (Arg13) 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_t4 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):

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

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_t7 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. ↩︎