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

Req: name + signature

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

Made with Slides.com