logoalt Hacker News

kjksf06/15/20252 repliesview on HN

I actually used the "virtual function" approach earlier in SumatraPDF.

The problem with that is that for every type of callback you need to create a base class and then create a derived function for every unique use.

That's a lot of classes to write.

Consider this (from memory so please ignore syntax errors, if any):

    class ThreadBase {
       virtual void Run();
       // ...
    }

    class MyThread : ThreadBase {
       MyData* myData;
       void Run() override;
       // ...
    }
    StartThread(new MyThread());
compared to:

    HANDLE StartThread(const Func0&, const char* threadName = nullptr);    
    auto fn = MkFunc0(InstallerThread, &gCliNew);
    StartThread(fn, "InstallerThread");

I would have to create a base class for every unique type of the callback and then for every caller possibly a new class deriving.

This is replaced by Func0 or Func1<T>. No new classes, much less typing. And less typing is better programming ergonomics.

std::function arguably has slightly better ergonomics but higher cost on 3 dimension (runtime, compilation time, understandability).

In retrospect Func0 and Func1 seem trivial but it took me years of trying other approaches to arrive at insight needed to create them.


Replies

mwkaufma06/16/2025

>> I would have to create a base class for every unique type of the callback and then for every caller possibly a new class deriving.

An interface declaration is, like, two lines. And a single receiver can implement multiple interfaces. In exchange, the debugger gets a lot more useful. Plus it ensures the lifetime of the "callback" and the "context" are tightly-coupled, so you don't have to worry about intersecting use-after-frees.

gpderetta06/16/2025

You could do:

    template<class R, class... Args>
    struct FnBase {
       virtual R operator()(Args...) = 0;
    };

    class MyThread : FnBase<void> { ... };
show 1 reply