Function Reference
My implementation of a functionRef/functionView variant.
Why? I desperately wanted to know just how std::function<> works. So I scoured the tutorials, books what have you, in order to find more information and ended up creating a sort of lightweight variant of it.
The skills you need to have under your belt before attempting this tutorial are:
- Type erasure.
- callable objects
- std::invoke
- higher order functions
- lambda calculus - just a smattering knowledge
Based on std::function, functionRef is a non-owning wrapper for an arbitrary callable object.
Implementation notes:
- A primary template is used to match the complete type of a callable, say
void( int, int )or int. The primary template remains an empty struct. - We want to differentiate the function type
Callableto its return type and argument type(s). Therefore partial template specialization is used. - The callable's type is deduced from the templated
functionRef's constructor. This is the only place where we can acquire information about the pointee (ie the callable object) type. - The
enable_ifs in the constructor constrain the class to accept Callable types only. - The third
enable_ifis to prevent the constructor hijacking the copy_constructor (comment it out and see test case 13. fail; you'll realize why it's needed then) void*type erasure drivesfunctionRefas doesstd::function(although std function plays with template-OO-virtual type erasure). The common interface required by all wrapped objects is they have to beCallables.- There are 2 members:
- a
void*data memberm_pAddrwhich represents the callable object. - a generic function pointer for a callable object
TReturn(*)(void*, TArgs...) m_pFn. This function pointer sets-up the INVOKE operation at the time of invocation of itsoperator().m_pFngets initialized by a stateless lambda; it gets m_pAddr and the arguments perfectly forwarded to it. In the case of a member functionm_pFnacquires its type later through thesetTargetmember call, which converts the supplied member function (pointer)fto a function pointer and stores it inm_pFn(yes this is hacky territory but works all the time - see this SO answer for more details). The two members both point to the exact same address in memory (in most architectures) but they are different representations for the specified operation on the address,m_pAddris a data pointer - ie. reads/write data at the address, the otherm_pFnis a function pointer - ie. executes instructions beginning at that address.
- a
- In contrast to
std::function,functionRefstores a pointer to its target, it doesn't copy/move/manage/own its target. If the target is not a glvalue it will vaporize after the call to the constructor returns (it may still be valid but it's UB territory). However we still want to be able to also capture such temporary callables, such as lambdas created on the fly. - If the constructor is passed an lvalue,
FTypeis an lvalue reference type, and thusFType*would be ill-formed. That is why I useadd_pointer_t<FType>such thatFType*would point to the actual object. - If the callable object is of const-qualified type, then
reinterpret_cast<void*>( addressof( callable ) )could not cast away the constness of the object (nor could implicitly convert tovoid*because that would drop const and C++ is strict on enforcing const-correctness rules). Thus we needconst_castfirst for that. This is why I use (void*) as the pointer to the object, to make sure that explicit conversion tovoid*happens for both const and non-const callables. - There is no need for templated
operator()in use withstd::forward, as the value categories of the arguments are already “locked in” as they were passed to the constructor - The destructor doesn't delete or invalidate the underlying object in any way (although it may not be possible to access it afterwards. Note that
functionRefis only a view/ref class, not an owner/manager class, likestd::function). The destructor destructs the wrapper iefunctionRefitself. We must make sure the pointee callable object lives at least as long as the wrapperfunctionRefobject. functionRefis able to call member functions too, including virtual member functions. The process requires an intermediate setup step though. The Class of the target object and the target member function are not supplied as part of the struct, because then we would have to store them and increasefunctionRef's size requirements permanently even if the target member was not a member function. Instead what we do, is call setup to the target member function and the target object to call with it, and only then call the function as a regular non-member function, throughfunctionRef, simply by supplying its arguments (as you would do with any other functionfunctionRefsupports).
For example:
you first have to:
myFunctionRef.setTarget( targetObj, &TargetClass::targetMemberFunction)
and then
myFunctionRef(args...)
- Although member functions are handled with no issues the best way to call member functions is to use a lambda that will capture the target object, or pass it as a parameter at the call site, and then call its member functions like so:
function_ref<void(T&)> ref( []( T& obj ){ obj.foo(); }); functionRefdoesn't “call” member data although they are in theory “Callables” - this is intentional
I provide numerous test cases in main.cpp which you can test for yourself.
I used Windows, Visual Studio 2017 and C++17 to build the project. I've also tested it working on Linux Ubuntu 16.04 & 18.04.
Github
Github repository link.
Acknowledgements
open-std function_ref proposal
Implementing function_view is harder than you might think - Foonathan blog
std::invoke, std::invoke_r - cppreference.com