Simplest C++ callback, from SumatraPDF
Ctrl + K
to search...
Home
Software
Contact Me

Simplest C++ callback, from SumatraPDF

SumatraPDF is a Windows GUI application written in C++ for viewing PDF, ePub and comic books.
A common need in GUI programs is a callback. E.g. when a button is clicked we need to call a function with some data identifying which button was clicked. Callback is therefore a combo of function and data and we need to call the function with data as an argument.
In programming language lingo, code + data combo is called a closure.
C++ has std::function<> and lambdas (i.e. closures). Lambdas convert to std::function<> and capture local variables.
Lambdas can be used as callbacks so problems solved?
Not for me.
I’ve used std::function<> and I’ve used lambdas and what pushed me away from them were crash reports.
I’ve implemented crash reporting and it’s been very useful.
The problem with lambdas is that they are implemented as compiler-generated functions. They get non-descriptive, auto-generated names. When I look at call stack of a crash I can’t map the auto-generated closure name to a function in my code. It makes it harder to read crash reports.

Simplest solution that could possibly work

You should know up front that my solution is worse than std::function<> in most ways. It’s not as nice to type as a lambda, it supports a small subset of std::function<> functionality.
On the other hand it’s small, fast and I can understand it.
One thing you need to know about me is that despite working on SumatraPDF C++ code base for 16 years, I don’t know 80% of C++.
I get by thanks to sticking to a small subset that I do understand.
I don’t claim I’ve invented this particular method. It seems obvious in retrospect but it did take me 16 years to arrive at it.
The code lives in src/base/Base.h.

Implementation of a simple callback in C++

A closure is code + data

A closure is conceptually simple. It combines code (function) and data:
using func0Ptr = void (*)(void*);
struct Func0 {
  func0Ptr fn;
  void* data;
  void Call() { fn(data); }
};
There are 2 big problems with this.
First is annoying casting. You have to do:
struct MyFuncData { };

void MyFunc(void* voidData) {
  MyFuncData* data = (MyFuncData*)voidData;
}

auto data = new MyFuncData;
auto fn = Func0{MyFunc, (void*)data};
Second is lack of type safety:
struct MyFuncData {};
struct MyOtherFuncData {};

void MyOtherFunc(void* voidData) {
  MyOtherFuncData* data = (MyOtherFuncData*)voidData;
}

auto data = new MyFuncData;
auto fn = Func0{MyOtherFunc, (void*)data};
We will call MyOtherFunc with data of MyFunc. This will likely crash.
The good thing is that pointer types are compatible. The machine instructions to call void Foo(void*) are exactly the same as calling void Foo(FooData*).
We can solve the above annoyances with a bit of cleverness in the form of MkFunc0():
template <typename T>
Func0 MkFunc0(void (*fn)(T*), T* d) {
    auto res = Func0{};
    res.fn = (void*)fn;
    res.userData = (uintptr_t)d;
    return res;
}

void MyFunc(MyFuncData* data) { }

auto data = new MyFuncData;
auto fn = MkFunc0(MyFunc, data);
We no longer need to cast data from void* in MyFunc.
Trying to create a mismatched auto fn = MkFunc0(MyFunc, new MyOtherFuncData) will result in an error. The compiler will notice that the fn and data arguments don’t match.
We’ll make one improvement: ability to also create a closure for functions without any arguments:
void MyFuncNoData() { }
Func0 fn = MkFunc0Void(MyFuncNoData);
The implementation cleverness: use a special, impossible value of a pointer to indicate a function without arguments. That sentinel is ~(uintptr_t)1 i.e. every bit set except the lowest one. That value is even. Why that matters becomes obvious when we get to Func1.
The full Func0 is:
using func0Ptr = void (*)(void*);
using funcVoidPtr = void (*)();

struct Func0 {
    // Func1 keeps a flag in userData's lowest bit, so every value stored there
    // has to be even - including this sentinel, which is why it is ~1 and not -1
    static constexpr uintptr_t kFuncNoArg = ~(uintptr_t)1;

    void* fn = nullptr;
    uintptr_t userData = 0;

    Func0() = default;
    Func0(const Func0& that) {
        this->fn = that.fn;
        this->userData = that.userData;
    }
    Func0& operator=(const Func0& that) {
        if (this != &that) {
            this->fn = that.fn;
            this->userData = that.userData;
        }
        return *this;
    }
    ~Func0() = default;

    bool IsValid() const { return fn != nullptr; }
    void Call() const {
        if (!fn) {
            return;
        }
        if (userData == kFuncNoArg) {
            auto func = (funcVoidPtr)fn;
            func();
            return;
        }
        auto func = (func0Ptr)fn;
        func((void*)userData);
    }
};

Func0 MkFunc0Void(funcVoidPtr fn);

template <typename T>
Func0 MkFunc0(void (*fn)(T*), T* d) {
    auto res = Func0{};
    res.fn = (void*)fn;
    res.userData = (uintptr_t)d;
    return res;
}
MkFunc0Void is a non-template so it lives in Base.cpp:
Func0 MkFunc0Void(funcVoidPtr fn) {
    auto res = Func0{};
    res.fn = (void*)fn;
    res.userData = Func0::kFuncNoArg;
    return res;
}

Closure with additional caller-provided argument

Func0 only addresses a use case of packaging a function and its own data.
Most of use cases for callbacks require passing additional arguments.
For example a list view control has onItemSelected(int itemIndex) callback.
For that we need Func1:
template <typename T>
struct Func1 {
    // bit 0 of userData says fn takes no T, so Call() drops the argument -
    // that's how a Func0 can stand in for a Func1. Everything we store is at
    // least 2-byte aligned (and kFuncNoArg is even), so the bit is free and the
    // struct stays two words
    static constexpr uintptr_t kDropsArgBit = 1;
    static constexpr uintptr_t kFuncNoArg = Func0::kFuncNoArg;

    void (*fn)(void*, T) = nullptr;
    uintptr_t userData = 0;

    Func1() = default;
    // a Func0 is a Func1 that doesn't look at its argument
    Func1(const Func0& that) {
        this->fn = (void (*)(void*, T))that.fn;
        this->SetData((void*)that.userData, true);
    }
    Func1(const Func1& that) {
        this->fn = that.fn;
        this->userData = that.userData;
    }
    Func1& operator=(const Func1& that) {
        if (this != &that) {
            this->fn = that.fn;
            this->userData = that.userData;
        }
        return *this;
    }
    ~Func1() = default;

    void SetData(void* d, bool dropsArg) {
        // an odd pointer would collide with the flag. Nothing we take the
        // address of is 1-byte aligned, so this means the caller handed us
        // something that isn't a real pointer
        ReportIf(((uintptr_t)d & kDropsArgBit) != 0);
        userData = (uintptr_t)d | (dropsArg ? kDropsArgBit : 0);
    }
    bool IsValid() const { return fn != nullptr; }
    void Call(T arg) const {
        if (!fn) {
            return;
        }
        uintptr_t d = userData & ~kDropsArgBit;
        if (userData & kDropsArgBit) {
            if (d == kFuncNoArg) {
                auto func = (funcVoidPtr)fn;
                func();
            } else {
                auto func = (func0Ptr)fn;
                func((void*)d);
            }
            return;
        }
        if (d == kFuncNoArg) {
            using fptr = void (*)(T);
            auto func = (fptr)fn;
            func(arg);
            return;
        }
        fn((void*)d, arg);
    }
};

template <typename T1, typename T2>
Func1<T2> MkFunc1(void (*fn)(T1*, T2), T1* d) {
    auto res = Func1<T2>{};
    using fptr = void (*)(void*, T2);
    res.fn = (fptr)fn;
    res.SetData((void*)d, false);
    return res;
}

template <typename T2>
Func1<T2> MkFunc1Void(void (*fn)(T2)) {
    auto res = Func1<T2>{};
    using fptr = void (*)(void*, T2);
    res.fn = (fptr)fn;
    res.SetData((void*)Func1<T2>::kFuncNoArg, false);
    return res;
}
Call() has 4 cases, packed into 2 words:
userData meaning calls
pointer MkFunc1 fn(data, arg)
kFuncNoArg MkFunc1Void fn(arg)
pointer | bit 0 Func0 assigned to Func1 fn(data) , arg dropped
kFuncNoArg | bit 0 MkFunc0Void assigned to Func1 fn() , arg dropped
We can now do:
struct OnListItemSelectedData { };

void OnListItemSelected(OnListItemSelectedData* d, int selectedIdx) {
}

struct ListView {
  Func1<int> onListItemSelected;
  void listItemSelected(int idx) {
    onListItemSelected.Call(idx);
  }
};

auto lv = new ListView;
auto data = new OnListItemSelectedData;
lv.onListItemSelected = MkFunc1(OnListItemSelected, data);
In Func0 the argument must be a pointer because the type is forgotten when we put it in a struct. We rely on the fact that void foo(void*) and void foo(Foo*) are compatible and we can cast the argument and function.
But Func1 retains the type of second argument so it can be any type and the right call will happen.
We also don’t want to erase the second type to avoid casts when calling it and to serve as documentation.
We could write Func2 for 2 arguments, Func3 for 3 arguments etc. but I didn’t bother. If I need more than one argument, I can always use struct to pack any number of arguments into a single one.

A Func0 is a Func1 that ignores its argument

GUI widgets usually expose Func1<Event*>. Sometimes the handler doesn’t care about the event. I still want to write onClick = MkFunc0(MyHandler, data) or onClick = MkFunc0Void(ClearThing).
So a Func0 converts to Func1<T>. Conversion sets the stolen bit, and Call(arg) drops arg.
That is why the no-arg sentinel is ~1 and not -1. -1 has bit 0 set, so SetData((void*)-1, true) would look like a colliding odd pointer and trip the assert. ~1 is even, so we can OR in the flag and still recover the sentinel by masking the bit off.
Stealing a bit also keeps sizeof(Func1<T>) at 16 bytes. A bool dropsArg member would pad the struct to 24.

Member functions

Most of my GUI handlers are methods on a window struct. I don’t want a free function plus a separately allocated data struct just to call this->OnOk().
The method pointer is a template argument, so it is baked into a trampoline at compile time and we still only store fn + this:
template <typename T, void (T::*Method)()>
static void MethodTrampoline(void* obj) {
    (static_cast<T*>(obj)->*Method)();
}

template <typename T, void (T::*Method)()>
Func0 MkMethod0(T* obj) {
    auto res = Func0{};
    res.fn = (void*)&MethodTrampoline<T, Method>;
    res.userData = (uintptr_t)obj;
    return res;
}

template <typename T, typename TArg, void (T::*Method)(TArg)>
static void MethodTrampoline1(void* obj, TArg arg) {
    (static_cast<T*>(obj)->*Method)(arg);
}

template <typename T, typename TArg, void (T::*Method)(TArg)>
Func1<TArg> MkMethod1(T* obj) {
    auto res = Func1<TArg>{};
    using fptr = void (*)(void*, TArg);
    res.fn = (fptr)&MethodTrampoline1<T, TArg, Method>;
    res.SetData((void*)obj, false);
    return res;
}
In real SumatraPDF code this looks like:
struct AddFavoriteWnd : WindowBase {
    void OnCancel(VirtMouseEvent* ev = nullptr);
    void OnOk(VirtMouseEvent* ev = nullptr);
};

btnCancel->onClick = MkMethod1<AddFavoriteWnd, VirtMouseEvent*, &AddFavoriteWnd::OnCancel>(this);
btnOk->onClick = MkMethod1<AddFavoriteWnd, VirtMouseEvent*, &AddFavoriteWnd::OnOk>(this);
wnd->onBeforeDelete = MkFunc0Void(ClearAddFavoriteWnd);
wnd->onClose = MkFunc1Void<WindowBase::CloseEvent*>(OnClose);
The crash stack then shows AddFavoriteWnd::OnOk (via a named trampoline), not a compiler-generated lambda.

Fringe benefits

So is it worth it to use this over std::function<>?
For me it does and SumatraPDF uses Func0 and Func1 instead of std::function<>.
Yes, std::function<> is better in many ways.
It’s more flexible. My solution only supports:
std::function<> supports arbitrary number of arguments of any type.
Compared to writing a lambda with variable capture, I need to write more code when the handler is not already a method:
I decided writing this boilerplate doesn’t bother me. For window methods, MkMethod0 / MkMethod1 remove most of it.
There are fringe benefits of my approach.
On MSVC 64-bit std::function<> is 64 bytes. Func0 and Func1 are 16 bytes.
Templated code is a highway to bloat. For every unique type, the compiler generates a new class definition and set of methods. Implementation of std::function<> is gigantic compared to Func0 and Func1.
Templated code is also a highway to slow compilation. Again, std::function<> is at least order of magnitude more complicated so it’ll take order of magnitude longer to compile.
Finally, I understand my implementation. I don’t understand std::function<> implementation. It’s scarier than Freddy Krueger. It’s scarier than Frankenstein’s monster.
In fact, I don’t think anyone understands std::function<> including the 3 people who implemented it.
#SumatraPDF #programming #c++
Aug 23 2026

Related articles

Feedback about page:

Feedback:
Optional: your email if you want me to get back to you: