[HN Gopher] Simplest C++ Callback, from SumatraPDF
       ___________________________________________________________________
        
       Simplest C++ Callback, from SumatraPDF
        
       Author : jandeboevrie
       Score  : 59 points
       Date   : 2025-06-15 17:26 UTC (5 hours ago)
        
 (HTM) web link (blog.kowalczyk.info)
 (TXT) w3m dump (blog.kowalczyk.info)
        
       | not-so-darkstar wrote:
       | https://web.archive.org/web/20250615181542/https://blog.kowa...
        
         | kjksf wrote:
         | It's back up now.
         | 
         | Somehow my blog server got overwhelmed and requests started
         | taking tens of seconds. Which is strange because typically it's
         | under 100ms (it's just executing a Go template).
         | 
         | It's not a CPU issues so there must be locking issue I don't
         | understand.
        
       | waynecochran wrote:
       | A small kitten dies every time C++ is used like its 1995.
       | void (*fn)(void*, T) = nullptr;
        
         | plq wrote:
         | Unless you mutter the magic incantation "C compatibility" while
         | doing it
        
         | zabzonk wrote:
         | did nullptr exist in c++ back in 1995 - i can't remember
        
           | trealira wrote:
           | Nope, it was introduced in C++11, along with the type
           | std::nullptr_t. Before that, you either used 0 or NULL, which
           | was a macro constant defined to be 0.
        
         | tom_ wrote:
         | And another one dies every time you need to step through a call
         | to std::function. Whatever you do, the kittens are never going
         | to escape.
        
       | JeanMarcS wrote:
       | Don't know about the code subtilities, but SumatraPDF is a gift
       | for viewing PDF on MS Windows. So big thanks to the author !
        
       | jitans wrote:
       | Why don't use fu2? https://naios.github.io/function2/
        
         | kjksf wrote:
         | One, I didn't know about it.
         | 
         | Two, my main objective is extreme simplicity and
         | understandability of the code.
         | 
         | I explicitly gave up features of std::function for smaller code
         | that I actually understand.
         | 
         | fu2 seems to be "std::function but more features".
        
       | mdaniel wrote:
       | SumatraPDF is outstanding software. But I'm actually surprised to
       | hear that it seems to be written in C++ ... I dunno, kind of like
       | "by default?" And a blog post hand rolling callback functions
       | using structs and a bunch of pointers seems to double down on:
       | are you sure this language is getting you where you want to go?
        
         | kjksf wrote:
         | As opposed to?
         | 
         | Today, if I was starting from scratch, I would try zig or odin
         | or maybe even Go.
         | 
         | But SumatraPDF started 15 years. There was nothing but C++. And
         | a much different C++ that C++ of today.
         | 
         | Plus, while my own code is over 100k lines, external C / C++
         | libraries are multiple of that so (easy) integration with C /
         | C++ code is a must.
        
           | mdaniel wrote:
           | I didn't know how to correctly package my comment as not
           | criticizing, and that's half of why I opened with "is
           | outstanding software." I genuinely believe that, I'm deeply
           | grateful for you releasing SumatraPDF into the world, and it
           | makes my life better. Truly, I am thankful
           | 
           | I hear you about "back in my day," but since as best I can
           | tell it's just your project (that is, not a whole team of 50
           | engineers who have to collaborate on the codebase) so you are
           | the audience being done a disservice by continuing to battle
           | a language that hates you
           | 
           | As for the interop, yes, since the 70s any language that
           | can't call into a C library is probably DoA but that list
           | isn't the empty set, as you pointed out with the ones you've
           | actually considered. I'd even _suspect_ if you tried Golang
           | it may even bring SumatraPDF to other platforms which would
           | be another huge benefit to your users
        
             | mhd wrote:
             | > I'd even suspect if you tried Golang it may even bring
             | SumatraPDF to other platforms which would be another huge
             | benefit to your users
             | 
             | Probably by using a cross-platform toolkit written in C++.
        
             | kjksf wrote:
             | Don't worry about being nice, 15 years doing open source
             | develops a thick skin.
             | 
             | But you didn't respond: which language should I use?
             | 
             | Go didn't exist when I started SumatraPDF.
             | 
             | And while I write pretty much everything else in Go and
             | love the productivity, it wouldn't be a good fit.
             | 
             | A big reason people like Sumatra is that it's fast and
             | small. 10 MB (of which majority are fonts embedded in the
             | binary) and not 100MB+ of other apps.
             | 
             | Go's "hello world" is 10 MB.
             | 
             | Plus abysmal (on Windows) interop with C / C++ code.
             | 
             | And the reason SumatraPDF is unportable to mac / linux is
             | not the language but the fact that I use all the Windows
             | API I can for the UI.
             | 
             | Any cross-platform UI solution pretty much require using
             | tens of megabytes of someone else's reimplementation of all
             | the UI widgets (Qt, GTK, Flutter) or re-implementing a
             | smaller subset of UI using less code.
        
               | sitzkrieg wrote:
               | sumatrapdf not being cross platform is a great feature,
               | maximizing the use of intended platform. win32api is
               | great. thank you for that
        
       | cherryteastain wrote:
       | Why not just pass around an
       | std::pair<void(*)(FuncData*), std;:unique_ptr<FuncData>>
       | 
       | at this stage? This implementation has a bunch of performance and
       | ergonomics issues due to things like not using perfect forwarding
       | for the Func1::Call(T) method, so for anything requiring copying
       | or allocating it'll be a decent bit slower and you'll also be
       | unable to pass anything that's noncopyable like an
       | std::unique_ptr.
        
         | kjksf wrote:
         | I don't know fancy C++ so I don't understand your point about
         | perfect forwarding.
         | 
         | But I do know the code I write and you're wrong about
         | performance of Func0 and Func1. Those are 2 machine words and
         | all it takes to construct them or copy them is to set those 2
         | fields.
         | 
         | There's just no way to make it faster than that, both at
         | runtime or at compile time.
         | 
         | The whole point of this implementation was giving up fancy
         | features of std::function in exchange for code that is small,
         | fast (both runtime and at compilation time) and one that I 100%
         | understand in a way I'll never understand std::function.
        
           | cherryteastain wrote:
           | In this function                   void Call(T arg) const {
           | if (fn) {                 fn(userData, arg);             }
           | }
           | 
           | Say you pass something like an std::vector<double> of size 1
           | million into Call. It'll first copy the std::vector<double>
           | at the point you invoke Call, even if you never call fn.
           | Then, if fn is not nullptr, you'll then copy the same vector
           | once more to invoke fn. If you change Call instead to
           | void Call(T&& arg) const {             if (fn) {
           | fn(userData, std::forward<T>(arg));             }         }
           | 
           | the copy will not happen at the point Call is invoked.
           | Additionally, if arg is an rvalue, fn will be called by
           | moving instead of copying. Makes a big difference for
           | something like                   std::vector<double> foo();
           | void bar(Func1<std::vector<double>> f) {             auto v =
           | foo();             f(std::move(v));         }
        
           | OskarS wrote:
           | > But I do know the code I write and you're wrong about
           | performance of Func0 and Func1. Those are 2 machine words and
           | all it takes to construct them or copy them is to set those 2
           | fields.
           | 
           | You also have to heap allocate your userData, which is
           | something std::function<> avoids (in all standard
           | implementations) if it's small enough (this is why the
           | sizeof() of std::function is larger than 16 bytes, so that it
           | can optionally store the data inline, similar to the small
           | string optimization). The cost of that heap allocation is not
           | insignificant.
           | 
           | If I were doing this, I might just go the full C route and
           | just use function pointers and an extra "userData" argument.
           | This seems like an awkward "middle ground" between C and C++.
        
       | _randyr wrote:
       | I'm not a C++ programmer, but I was under the impression that
       | closures in c++ were just classes that overload the function call
       | operator `operator()`. So each closure could also be implemented
       | as a named class. Something like:                   class
       | OnListItemSelected {             OnListItemSelectedData data;
       | void operator()(int selectedIndex) { ... }         }
       | 
       | Perhaps I'm mistaken in what the author is trying to accomplish
       | though?
        
         | OskarS wrote:
         | Indeed, that is exactly the case, lambdas are essentially
         | syntax sugar for doing this.
         | 
         | The one thing the author's solution does which this solution
         | (and lambdas) does not is type erasure: if you want to pass
         | that closure around, you have to use templates, and you can't
         | store different lambdas in the same data structure even if they
         | have the same signature.
         | 
         | You could solve that in your case by making `void operator()`
         | virtual and inheriting (though that means you have to heap-
         | allocate all your lambdas), or use `std::function<>`, which is
         | a generic solution to this problem (which may or may not
         | allocate, if the lambda is small enough, it's usually optimized
         | to be stored inline).
         | 
         | I get where the author is coming from, but this seems very much
         | like an inferior solution to just using `std::function<>`.
        
         | spacechild1 wrote:
         | Exactly! And if you need type erasure, you can just store it in
         | a std::function.
         | 
         | > OnListItemSelectedData data;
         | 
         | In this case you can just store the data as member variables.
         | No need for defining an extra class just for the data.
         | 
         | As I've written elsewhere, you can also just use a lambda and
         | forward the captures and arguments to a (member) function. Or
         | if you're old-school, use std::bind.
        
       | noomen wrote:
       | I just want to thank SumatraPDF's creator, he literally saved my
       | sanity from the evil that Adobe Acrobat Reader is. He probably
       | saved millions of people thousands of hours of frustration using
       | Acrobat Reader.
        
       | pjmlp wrote:
       | Another example of NIH, better served by using the standard
       | library.
        
       | spacechild1 wrote:
       | > I've used std::function<> and I've used lambdas and what pushed
       | me away from them were crash reports.
       | 
       | In danger of pointing out the obvious: std::function does note
       | require lambdas. In fact, it has existed long before lambdas
       | where introduced. If you want to avoid lambdas, just use
       | std::bind to bind arguments to regular member functions or free
       | functions. Or pass a lambda that just forwards the captures and
       | arguments to the actual (member) function. There is no reason for
       | regressing to C-style callback functions with user data.
        
         | mandarax8 wrote:
         | std::bind is bad for him for the same reasons std::function is
         | bad though
        
           | spacechild1 wrote:
           | Why? If the bound (member) function crashes, you should get a
           | perfectly useable crash report. AFAIU his problem was that
           | lambdas are anonymous function objects. This is not the case
           | here, because the actual code resides in a regular (member)
           | function.
        
             | dustbunny wrote:
             | Does a stack trace from a crash in a bound function show
             | the line number of where the bind() took place?
        
               | delusional wrote:
               | Assuming the stack trace is generated by walking up the
               | stack at the time when the crash happened, nothing that
               | works like a C function pointer would ever do that.
               | Assigning a a pointer to a memory location doesn't
               | generate a stack frame, so there's no residual left in
               | the stack that could be walked back.
               | 
               | A simple example. If you were to bind a function pointer
               | in one stack frame, and the immediately return it to the
               | parent stack frame which then invokes that bound pointer,
               | the stack that bound the now called function would
               | literally not exist anymore.
        
               | spacechild1 wrote:
               | No, but neither does the author's solution.
        
         | kjksf wrote:
         | I did use bind earlier in SumatraPDF.
         | 
         | There are 2 aspects to this: programmer ergonomics and other
         | (size of code, speed of code, compilation speed,
         | understandability).
         | 
         | Lambdas with variable capture converted to std::function have
         | best ergonomics but at the cost of unnamed, compiler-generated
         | functions that make crash reports hard to read.
         | 
         | My Func0 and Func1<T> approach has similar ergonomics to
         | std::bind. Neither has the problem of potentially crashing in
         | unnamed function but Func0/Func1<T> are better at other
         | (smaller code, faster code, faster compilation).
         | 
         | It's about tradeoffs. I loved the ergonomics of callbacks in C#
         | but I working within limitations of C++ I'm trying to find
         | solutions with attributes important to me.
        
       | mandarax8 wrote:
       | What he shows here is 75% of c++26's std::function_ref. It's
       | mainly missing variadic arguments and doesn't support all types
       | of function objects.
       | 
       | https://github.com/TartanLlama/function_ref/blob/master/incl...
        
         | kjksf wrote:
         | I can honestly say that I couldn't write that thing in 100
         | years.
         | 
         | I can't even read it.
         | 
         | That's the fundamental problem with C++: I've understood pretty
         | much all Go code I ever looked at.
         | 
         | The code like the above is so obtuse that 0.001% of C++
         | programmers is capable of writing it and 0.01% is capable of
         | understanding it.
         | 
         | Sure, I can treat it as magic but I would rather not.
        
           | spacechild1 wrote:
           | Do you understand how your compiler works? Shouldn't you be
           | writing assembly instead? You can't understand all internals
           | and that's perfectly fine.
           | 
           | Why do you even care how std::function is implemented?
           | (Unless you are working in very performance critical or
           | otherwise restricted environments.)
        
             | kjksf wrote:
             | I've listed several reasons why I decided to write and use
             | this implementation:                 - better call stacks
             | in crash reports       - smaller and faster at runtime
             | - faster compilation because less complicated, less
             | templated code       - I understand it
             | 
             | So there's more to it that just that one point.
             | 
             | Did I loose useful attributes? Yes. There's no free lunch.
             | 
             | Am I going too far to achieve small, fast code that
             | compiles quickly? Maybe I do.
             | 
             | My code, my rules, my joy.
             | 
             | But philosophically, if you ever wonder why most software
             | today can't start up instantly and ships 100 MB of stuff to
             | show a window: it's because most programmers don't put any
             | thought or effort into keeping things small and fast.
        
               | spacechild1 wrote:
               | Oh, I definitely agree with some of your other points,
               | just not the one I argued against.
               | 
               | BTW, I would also contest that your version is faster at
               | runtime. Your data always allocated on the heap.
               | Depending on the size of the data, std::function can
               | utilize small function optimization and store everything
               | in place. This means there is no allocation when setting
               | the callback and also better cache locality when calling
               | it. Don't make performance claims without benchmarking!
               | 
               | Similarly, the smaller memory footprint is not as clear
               | cut: with small function optimization there might be
               | hardly a difference. In some cases, std::function might
               | even be smaller. (Don't forget about memory allocation
               | overhead!)
               | 
               | The only point I will absolutely give you is compilation
               | times. But even there I'm not sure if std::function is
               | your bottleneck. Have you actually measured?
        
           | mandarax8 wrote:
           | Yeah it's a shame that to go from your idea to something
           | that's 'general' (ie just some arbitrary arguments) you need
           | to write this arcane garbage.
        
       | commandersaki wrote:
       | I always love this author's writing style, his articles are pure
       | bliss to read.
        
       | mwkaufma wrote:
       | The lengths some go to avoid just using a bog-standard virtual
       | function.
        
         | kjksf wrote:
         | 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.
        
       | mgaunard wrote:
       | Should have just implemented his own std::function with the
       | simplicity and performance trade-off he wanted.
        
       | akdev1l wrote:
       | I don't really understand what problem this is trying to solve
       | and how the solution is better than std::function. (I understand
       | the issue with the crash reports and lambdas being anonymous
       | classes but not sure how the solution improved on this or how
       | std::function has this problem?)
       | 
       | I haven't used windows in a long time but back in the day I
       | remember installing SumatraPDF to my Pentium 3 system running
       | windows XP and that shit rocked
        
       ___________________________________________________________________
       (page generated 2025-06-15 23:00 UTC)