Calling a function, how hard can it be?
Talk content
- Overview of various ways to call function pointers.
- Delegate, an alternative to std::function.
What is a function call?
void fkn1() {
cout << 0 << endl;
}
void fkn1(int x) {
cout << x << endl;
}
void fkn2(int x) {
cout << 2*x << endl;
}
int main() {
// Specify both function name, signature and make the call.
// or, deduce function address from signature and name, call address.
// This is overload resolution in standard language.
fkn1(1);
}
Some notation
- Function signature: The type of the function includes a number of parameters and their types.
- Function pointer: Object with the address of a function. Its type is given by the function signature.
- Function definition: Supplies a body to a function name with a particular signature.
- Function address: Can be taken from a function definition and later be used to call the function with arguments.
- Function arguments: Values to pass into a function at call time.
- Overload set: A set of functions sharing the same name, but with different signatures.
void fkn1() {
cout << 0 << endl;
}
void fkn1(int x) {
cout << x << endl;
}
void fkn2(int x) {
cout << 2*x << endl;
}
int main() {
// Specify signature, part of the type system.
using Fkn = void(*)(int);
// make function pointer.
Fkn fkn = nullptr;
// Calculate address from name together with signature.
// Overload resolution is performed. Assign to pointer.
fkn = &fkn1;
// Do the call. Address and argument values
// needed at the call site. No resolution done.
fkn(2);
}
Using a function pointer
Function names
- Set at compile time. Name + signature identify a unique function and give an address.
- Usage (direct call or assigning to pointer) is compile-time fixed. Can be used as a template argument.
- Once a function address is assigned to function pointer, it can manipulated as any value type at runtime.
- Calling a function pointer is a form of information hiding. We do not need to know the actual function at the call site. Only the variable and signature.
Fkn pointer construction
Req: Fkn signature
Assign address
or address
Fkn call:
Req: signature + arguments
Fkn pointer destruction
Time
struct C1 {
static void fkn1(C1* obj) {
cout << 1 << endl;
}
static void fkn2(C1* obj) {
cout << 2 << endl;
}
};
int main() {
// static member function works as free functions. Just
// declared within a class and have the class name as part of their name.
C1::fkn1(nullptr);
// Specify signature when setting up a function pointer type.
using Fkn = void(*)(C1*);
// Use name (including class name) with signature to extract address.
Fkn fkn = &C1::fkn1;
// and finally call.
C1 c1;
fkn(&c1);
}
Function pointer to class static functions
struct C1 {
static void fkn1(C1 const* c1) {
cout << 1 << endl;
}
void member() const {
cout << x << endl;
}
};
int main() {
// Static function signature as before.
using Fkn = void (*)(C1 const*);
Fkn freeFkn = &C1::fkn1;
// Member function
// Good analogy: the object pointer is a hidden first argument.
// Note: object pointer type is part of the signature,
// just as first argument in free function pointer.
using MemberFkn = void (C1::*)() const;
MemberFkn mFkn = &C1::member;
C1 c1;
// Call on fkn pointers. Require fkn address and signature.
freeFkn(&c1);
// Note the quirky syntax. Required.
(c1.*mFkn)();
}
Member function pointers
struct B1 {
virtual void member() {
cout << 1 << endl;
}
};
struct D1 : public B1 {
void member() {
cout << 2 << endl;
}
};
int main() {
D1 d1;
// Specify signature to base class member function.
using MemPtr = void (B1::*)();
// Get base member function pointer.
MemPtr ptr = &B1::member;
// Get a base pointer.
B1* b_ptr = &d1;
// Member function special power: Do dynamic dispatch. Calls D1::member.
(b_ptr->*ptr)();
}
Member pointer secret power: dynamic dispatch
struct F1 {
F1(int m) : member(m) {}
void operator()(int i) {
cout << i + member << endl;
}
void operator()() {
cout << m << endl;
}
int member;
};
int main() {
// Functors are ordinary classes
F1 f1{4};
// That can be called as functions.
f1(3);
f1();
// Can create references and pointers:
F1* ptr = &f1;
F1& ref = f1;
// And call through them.
(*ptr)();
ref();
}
// However there is no special function pointer syntax for functors.
Functors: objects with operator()
struct F1 {
F1(int m) : member(m) {}
void operator()(int i) {
cout << i + member << endl;
}
int member;
};
int main() {
// Functors are ordinary classes
int m = 4;
F1 f1{4};
auto lambda = [m](int i) -> void { cout << i + m << endl; }
// Lambda is defined to construct a templated functor.
// F1 is similar to the functor class made from lambda.
f1(3);
lambda(3);
}
Lambdas
struct F1 {
F1(int m) : member(m) {}
void operator()(int i) {
cout << i + member << endl;
}
int member;
};
int main() {
F1 f1(4);
// Store a copy of the functor.
std::function<void(int)> fknStore = f1;
// Call the function store.
// Note: Do not need to know the type of the functor, only signature.
fknStore(3);
// std::function implement type-erasure to allow storing full type
// but only exposing signature at call time.
}
std::function and type erasure
std::function
- Will automatically handle free functions, Functors, and lambdas. (Not member functions, use lambda)
- Great for general purpose storage of callables
- A convenient way to get type erasure between assignment and call point.
- Can heap allocate for object storage. Can throw exceptions
- Can use virtual functions to implement type erasure (might be slow).
Common scenario in embedded
- You have some driver with interrupt functionality. You want callbacks. But can not afford exceptions or heap allocation.
- The callback usually require some stored context pointer.
- We want type safety and would be good to call a member function on a particular object.
// Common C function solution.
// Driver code. Offers a callback without knowing callers type.
typedef void (*DrvCB)(void* ctx, int cbArg);
void drv_registerCB(DrvCB cb, void* context);
// User of the driver:
struct UserData { /* ... */ };
void userCodeInit(struct UserData* ud)
{
// Init ctx ...
drv_registerCB(drvCB, (void*)ud);
// ...
}
static void drvCB(void* ctx, int cbArg)
{
// Required cast from void to regain context type.
struct UserData* ud = (struct UserData*)ctx;
// Do callback processing...
}
// Direct old style C++ conversion.
class Driver {
public:
using DrvCB = void (*)(void* ctx, int cbArg);
void registerCB(DrvCB cb_, void* ctx_) {
cb = cb_;
cbCtx = ctx_;
}
private:
DrvCB cb;
void* cbCtx;
};
class UserClass
{
public:
UserClass(Driver* drv_p)
{
drv_p->registerCB(drvCB_, (void*)this);
// ...
}
private:
static void drvCB_(void* ctx, int arg) {
static_cast<UserClass*>(ctx)->drvCB(arg);
}
void drvCB(int arg) {
// Actual CB processing
}
}
A std::function replacement
- Create a std::function alternative. Let us call it 'delegate'.
- Borrow the idea with a static->member function adapter. Encapsulate this into the delegate.
- A function address is a compile-time constant. It can be used as a template argument.
- To avoid heap allocation, require the user to have storage for stateful Functors.
- Have support for member functions.
class Delegate
{
public:
using AdapterFkn = void (*)(void*ctx,int arg);
// New set method. Handles member function to object.
template <class T, void (T::*memFkn)(int arg)>
void set(T& object)
{
cb = &doMemberCB<T, memFkn>;
ctx = static_cast<void*>(&object);
}
void operator()(int t) {
cb(t);
}
private:
// Handler function for memeber callbacks. Same internal signature
// Other ways to unpack the template parameters.
template <class T, void (T::*memFkn)(int)>
inline static void doMemberCB(void* o, int arg)
{
T* obj = static_cast<T*>(o);
((*obj).*(memFkn))(arg);
}
AdapterFkn cb = nullptr;
void* ctx = nullptr;
};
Delegate handling of a member function
class Driver {
public:
Delegate& regCB();
private:
void isrProc() {
// ...
del(value);
}
// Note: no type information about the registered caller. It
// is handled in the 'set' function template. It does not leak
// to the delegate class. (Later, will be: delegate<void(int)>)
Delegate del;
};
class UserClass
{
public:
UserClass(Driver& drv)
{
drv.regCB().set<UserClass, &UserClass::drvCB>(*this);
// ...
}
private:
// Trampoline fkn now in delegate. Only need memeber fkn.
void drvCB(int arg) {
// Actual CB processing
}
}
Use of delegate
Delegate
- Straight-forward to generalize with argument parameter packs and return value.
- Proper use of inlining will remove any overhead of the adapter function. Both operator() and the adapter-function inlined to call site.
- Easy to add more set/adapter functions for normal function pointers / functors.
- Stateless lambdas are guaranteed to be convertible to function pointers and can be handled.
- No possible way for the delegate to throw an exception. (User code can though.)
class Delegate
{
public:
using AdapterFkn = void (*)(void*ctx,int arg);
template<typename T>
void setFunctor(T& f) {
cb = &adaptFunctor<T>;
ctx = static_cast<void*>(&t);
}
void operator()(int t) {
cb(ctx, t);
}
private:
template<class T>
static void adaptFunctor(void* ctx_, int arg) {
(*static_cast<T*>(ctx_))(arg);
}
AdapterFkn cb = nullptr;
void* ctx = nullptr;
};
Delegate handling a functor
Delegate as a library class
- Aim for the expressive power of std::function, feel like a free function pointer in use. (With less quirky syntax)
- Delegate should only contain type information about the calling signature.
- Model function pointers in terms of const correctness, value semantics, comparing to nullptr etc.
- Predictable efficient behavior. No own exceptions, no heap allocation, but still with type safety. Only 2 pointers used for storage.
- Tradeoff: User need to keep track of referred object lifetimes. (Like use of any pointer).
#include "delegate/delegate.hpp"
struct Packet { /* ... */ };
class Driver {
public:
Driver() = default;
using RxDelegate = delegate<void(const Packet&)>;
RxDelegate& registerRx() { return packetRx; } ; // Either this,
void setRx(RxDelegate rx) { packetRx = rx; }; // Or this.
private:
RxDelegate packetRx;
};
class ProtocolRx {
public:
ProtocolRx(Driver& drv) {
drv.registerRx().set<
ProtocolRx, &ProtocolRx::rxPacket>(*this); // Either this,
drv.setRx(Driver::RxDelegate::make<
ProtocolRx, &ProtocolRx::rxPacket>(*this); // Or this
drv.registerRx().set<&ProtocolRx::rxPacket>(*this); // C++17
}
private:
void rxPacket(Packet const& packet)
{ /* ... */ }
};
#include "delegate/delegate.hpp"
struct TStruct {
int member(int i) { return i + 1; }
int cmember(int i) const { return i + 2; }
};
TEST(delegate, Member_intermediate_storage) {
TStruct ts;
const TStruct cts;
delegate<int(int)> del;
del.set<TStruct, &TStruct::member>(ts);
int res = del(1);
EXPECT_EQ(res, 2);
// Must not compile. Need const member for const object.
// del.set<TStruct, &TStruct::member>(cts);
del.set<TStruct, &TStruct::cmember>(ts);
res = del(1);
EXPECT_EQ(res, 3);
del.set<TStruct, &TStruct::cmember>(cts);
res = del(1);
EXPECT_EQ(res, 3);
// Must not compile. Do not allow storing pointer to temporary.
// del.set<TStruct, &TStruct::member>(TStruct{});
// del.set<TStruct, &TStruct::cmember>(TStruct{});
}
Const correctness
Const member functions
- Member function const determine if a function can be called on const objects or not. This can be checked during {set, make} calls. Once done, it is not needed anymore for doing a call.
- This requires us to set the member function and the object at the same time for const correctness.
- Use another class MemFkn to store a member function without an object. Require additional type information for const. (skipping volatile...)
- Same with functors w.r.t operator(), but no issue since there is only one object here.
#include "delegate/delegate.hpp"
TEST(delegate, construction1) {
delegate<void()> del;
delegate<void()> del2;
EXPECT_TRUE(del.null());
EXPECT_FALSE(del);
EXPECT_TRUE(del == nullptr); // And variants.
EXPECT_TRUE(del == del2);
EXPECT_TRUE(del.equal(del2));
EXPECT_FALSE(del.less(del2));
// Ok to call null delegate. Call function that do nothing and
// return default constructed return value.
del();
}
Default construct and safe nullptr call
#include "delegate/delegate.hpp"
// Base, Derived defined as expected, virtual memb, cmemb.
TEST(delegate, test_virtual_dispatch)
{
Derived d;
delegate<int(int)> del;
Base& b = d;
del.set<Base, &Base::memb>(b);
// Derived::memb called.
del(1);
const Base& cb = d;
del.set<Base, &Base::cmemb>(cb);
// Derived::cmemb called.
del(1);
}
Summary
-
delegate should work well in most settings where std::function is used today. It emulates member function pointers which std::function does not. - Takes function names as template arguments, providing better opportunities for optimization/inlining for the compiler. (todo: performance measurement...)
Available at:
https://github.com/rosbacke/delegate
Embedded_delegates
By mikael rosbacke
Embedded_delegates
- 398