Callable – a not notorious concept
In C++ we have the concept of callable which is any type for which operations INVOKE and INVOKE<R> hold. A not common concept that rarely arises on common daily workflow but powerful when you start playing with template meta-programming, and broad generalized solutions like building libraries and frameworks.
In this series we will explore the callable concept, what are their types and how to reference them. Once their types are known we then create a solution to detect not only which type of callable we are handling but also parse their components into useful pieces. In this first part the callable concept is introduced along side particular instances of interest.
But why would one need to know which type of callable one is dealing with before computing something? Well, broadly and very generally speaking a callable is some sort of a function (well, one of the types is actually a Function), in other words it can be used to form an expression like f(arg0, arg1, …., argN). As an appetizer one possible application that would handle callables is an asynchronous processing network where we store a reference for a callable and also the data it uses to only execute when resources are available.
The main objective here is to explain how to be able to use C++’s own machinery to determine during compile time if we are dealing with
- a Function,
- an Object’s Member Function.
- a Functor, or
- a Lambda Expression.
This distinction is specially important for the case of the Object’s Method that demands an extra argument when used — an object of its owner class type.
The approach I use is rather simplistic, but for the intended purpose is enough (KISS). The idea is to use partial template specialization technique to inspect what is we are dealing with. A pointer to a class/struct data member is also a callable, but will not be considered for our case, because we are considering only cases where a function like type is called.
Functions
Let’s start learning the type of the callables we are looking for. Starting with Function, we know it has a return type, a name and a list of parameters, e.g.,
int foo (int goo) {return ++goo;}
Although simple, the example above brings a complete function named foo that receives an int parameter by copy named goo, and returns another int that is the increment of goo. But what is the type of the function itself? I am not referring the return type, I talking about the whole thing. How would you express the function type if you need to reference to a given function later own not knowing its name? — We will not discuss when to use function pointer or a function type itself, there is a difference and the reader is encouraged to look at it.
Assuming you are using C++11 the easiest way would be to do like this
auto foo_ptr = &foo
Note we are using a pointer in this case — & is on the right hand side denoting we are using the function’s address. We can also use
decltype(&foo) foo_ptr= &foo;
where decltype() is the specifier to retrieve the type of its argument. In both ways we do not know explicitly the type being used, we are just handling over to the compiler to use the real type under the hood. The type we are looking for is this
int (*)(int)
Observe the concrete function pointer type is similar to the function signature, the difference is the absence of function name and parameter name and “*” is there to represent the pointer. Another peculiarity is the position of the pointer operand, in a common pointer it would be used after the end of the type name like in
int *int_ptr = &previous_int;
but the function pointer places it after the return type and before the function parameter type list. For the sake of curiosity, here is an example of how to use a function pointer.
#include <iostream>
int foo (int goo) {return ++goo;}
int too (int qoo) {return ++qoo;}
int main()
{
int (*foo_ptr)(int) = &foo;
int result = foo_ptr(1);
std::cout << "Result foo: " << result << std::endl; // Result foo: 2
int (*too_ptr)(int) = &too;
result = too_ptr(result);
std::cout << "Result too: " << result << std::endl; // Result too: 3
return 0;
}
A function pointer is defined on line 9. Notice the placement of the name of the function pointer. Line 11 uses pointer foo_ptr to call foo and stores the increment in result. Line 15 uses the same pointer type to point to another function goo that has the same signature. The increment is stored in line 17.
Instead of dealing with a function pointer type, we can also use a function reference type (&) like the code below
#include <iostream>
typedef int (&myFuncTypeRef)(int);
int foo (int goo) {return ++goo;}
int too (int qoo) {return ++qoo;}
int main()
{
int (&foo_ref)(int) foo_ref = foo;
int result = foo_ref(1);
std::cout << "Result foo: " << result << std::endl; // Result foo: 2
myFuncTypeRef too_ref = too;
result = too_ref(result);
std::cout << "Result too: " << result << std::endl; // Result too: 3
return 0;
}
Observe in line 3 of the previous code that the typedef defines the new type of the function pointer named myFuncTypeRef and it is used in line 17. The definition of the typedef follows the pattern presented previously for the function pointer. To use the newly defined type just write like when using a normal type, see line 17. Line 11 uses the same format as the pointer version but changed to reference.
We can put then the function type in a more generic format using a “EBNF“-ish form like the one below (this is not a strict notation by any measure), that says a function type is composed of a return type; it can be function, a function pointer, a function reference, a reference to a pointer or even an rvalue pointer. The last two cases can occur, for instance, when passing a function pointer to a universal reference and passing the address of a function reference also to a universal reference, respectively.
<return type> ([*|&|*&|*&&])(<list of parameter types>)
Object’s Member Function
This section will show the type used to reference a method inside an object, the member function pointer. There is a catch here, this type of pointer is not an absolute pointer, but rather an offset of the address of the member function inside the class definition. In order the be able to access such member function, its pointer has to be used on behalf of an object that will provide the this pointer. Assuming the reader has read the Function section, I will jump straight to an example in code below showing how we can use the member function pointer.
#include <iostream>
struct Foo{
int increment(int goo){return ++goo;}
};
int main()
{
Foo foo;
int (Foo::*increment_member_fcn_ptr)(int) = &Foo::increment;
int result = (foo.*increment_member_fcn_ptr)(1);
std::cout << "Result foo: " << result << std::endl; // Result foo: 2
Foo too;
result = (too.*increment_member_fcn_ptr)(result);
std::cout << "Result too: " << result << std::endl; // Result too: 3
return 0;
}
The type of the member function can be seen first in line 12. Note the name of the pointer variable goes “inside” the member function type — the same approach can be used for function pointers. Another detail is that because we are dealing with an offset, the address is taken with respect to the the struct definition and not a instantiated object like foo in line 10.
Line 14 is uses the pointer alongside an instantiated object of Foo, in this case foo. The result is stored and printed. Lines 18-22 repeat the same idea, but with a different object of Foo, goo. This shows that the same pointer can be used for two different concrete objects of the same type. Again we use an “EBNF“-ish form to represent the general case. Cases that use reference to pointer or rvalued pointer are related to passing the member function as an argument to a universal reference, for example.
<return type> (<Object Type>::*[&|&&])(<list of parameter types>) [const]
Functors and Lambda Expression
We finally arrive at the last two cases. I’m putting them together because a lambda expression is just a convenient way to define a nameless local functor. For the sake of completeness — a functor is a class/struct that overloads operator() so it can be called as function, like the code below
struct Foo{
int operator()(int goo) const {return ++goo;}
};
//...
Foo foo;
int result = foo(1);
std::cout << "Result foo: " << result << std::endl; // Result foo: 2
And because Functors and Lambdas are objects, it is the member function operator() that differentiate them from a regular class. The callable here is the functor itself, and it is characterized by the member function operator(), which can be referenced by a pointer like a member function pointer.
int (Foo::*op_member_fcn_ptr)(int) = &Foo::operator();
If C++17 is considered, Functors and Lambdas are treated different by std::invoke from regular member functions. This difference will also be considered when building our callable parser.
Extra
One situation that knowing the actual type to reference a function is when one has multiple functions with the same signature that are required to be called in sequence. One can create an array of the function type and iterate over all functions at once, like in the example below.
#include <iostream>
#include <array>
int increment1(int num){return ++num;}
int increment2(int num){return ++num;}
int increment3(int num){return ++num;}
using increment_function_array_t = std::array<int (*)(int), 3>;
int main()
{
increment_function_array_t func_array{&increment1, &increment2, &increment3};
int result = 0;
int zero = 0;
for(auto elem : func_array)
result += elem(zero);
std::cout << "Result: " << result << std::endl; // Result: 3
return 0;
}
Lines 4-6 defines three simple function that do the same thing, increment the received parameter in one and return the result. Line 8 defines an array type that has 3 elements of type int (*)(int). Then an array is created in line 13 containing a pointer to each of the previous defined increment function.
The resultant value is created and named result and initialized with 0. An auxiliary value zero also is initialized with 0.
Lines 18 and 19 present a for-each loop iterating over the function pointer array func_array and calls each element of the array, storing the computation in result. Line 21 prints the result.
Coming Up
In the next part I will show how to parse a Function, getting information regarding its return type, the list of parameters and how long is this list.
Stay tuned!