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
Callable
to 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_if
s in the constructor constrain the class to accept Callable types only. - The third
enable_if
is 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 drivesfunctionRef
as doesstd::function
(although std function plays with template-OO-virtual type erasure). The common interface required by all wrapped objects is they have to beCallable
s.- There are 2 members:
- a
void*
data memberm_pAddr
which 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_pFn
gets initialized by a stateless lambda; it gets m_pAddr and the arguments perfectly forwarded to it. In the case of a member functionm_pFn
acquires its type later through thesetTarget
member call, which converts the supplied member function (pointer)f
to 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_pAddr
is a data pointer - ie. reads/write data at the address, the otherm_pFn
is a function pointer - ie. executes instructions beginning at that address.
- a
- In contrast to
std::function
,functionRef
stores 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,
FType
is 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_cast
first 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
functionRef
is only a view/ref class, not an owner/manager class, likestd::function
). The destructor destructs the wrapper iefunctionRef
itself. We must make sure the pointee callable object lives at least as long as the wrapperfunctionRef
object. functionRef
is 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 functionfunctionRef
supports).
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(); });
functionRef
doesn'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