from Hacker News

Simplest C++ Callback, from SumatraPDF

by jandeboevrie on 6/15/25, 5:26 PM with 169 comments

  • by vanderZwan on 6/16/25, 4:07 AM

    I'm surprised that many comments here seem to have missed this bit of context:

    > 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'm pretty sure that most "why don't you just use x…" questions are implicitly answered by it, with the answer being "because using x correctly requires learning about all of it's intricacies and edge-cases, which in turn requires understanding related features q, r, s… all the way to z, because C++ edge-case complexity doesn't exist in a vacuum".

  • by JeanMarcS on 6/15/25, 6:58 PM

    Don't know about the code subtilities, but SumatraPDF is a gift for viewing PDF on MS Windows. So big thanks to the author !
  • by skrebbel on 6/16/25, 7:11 AM

    This seems very similar to Java's oldschool single-interface callback mechanism. Originally, Java didn't have lambdas or closures or anything of the sort, so instead they'd litter the standard library with single-method interfaces with names like ActionListener, MouseListener, ListItemSelectedListener, etc. You'd make a class that implements that interface, manually adding whatever data you need in the callback (just like here), and implement the callback method itself of course.

    I think that has the same benefit as this, that the callbacks are all very clearly named and therefore easy to pick out of a stack trace.

    (In fact, it seems like a missed opportunity that modern Java lambdas, which are simply syntactical sugar around the same single-method interface, do not seem to use the interface name in the autogenerated class)

  • by _randyr on 6/15/25, 7:52 PM

    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?
  • by tlb on 6/16/25, 10:04 AM

    I don't have this problem with backtraces in Clang. The 'anonymous' lambdas have debugging symbols named after the function it lexically appears in, something like parent_function::$_0::invoke. $_0 is the first lambda in that function, then $_1, etc. So it's easy enough to look up.
  • by comex on 6/16/25, 4:31 AM

    Note that some CFI (control flow integrity) implementations will get upset if you call a function pointer with the wrong argument types:

    https://gcc.godbolt.org/z/EaPqKfvne

    You could get around this by using a wrapper function, at the cost of a slightly different interface:

        template <typename T, void (*fn)(T *)>
        void wrapper(void *d) {
            fn((T *)d);
        }
    
        template <typename T, void (*fn)(T *)>
        Func0 MkFunc0(T* d) {
            auto res = Func0{};
            res.fn = (void *)wrapper<T, fn>;
            res.userData = (void*)d;
            return res;
        }
    
        ...
    
        Func0 x = MkFunc0<int, my_func>(nullptr);
    
    (This approach also requires explicitly writing the argument type. It's possible to remove the need for this, but not without the kind of complexity you're trying to avoid.)
  • by akdev1l on 6/15/25, 10:06 PM

    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

  • by noomen on 6/15/25, 7:53 PM

    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.
  • by spacechild1 on 6/15/25, 8:23 PM

    > 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.

  • by mwkaufma on 6/15/25, 9:48 PM

    The lengths some go to avoid just using a bog-standard virtual function.
  • by waynecochran on 6/15/25, 6:48 PM

    A small kitten dies every time C++ is used like its 1995.

        void (*fn)(void*, T) = nullptr;
  • by bmn__ on 6/16/25, 11:28 AM

    > [Lambdas] 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.

    Well, HN lazyweb, how do you override the stupid name in C++? In other languages this is possible:

      $ node --trace-uncaught -e 'const c = function have_name() {throw null}; c()'
    
      $ perl -d:Confess -MSub::Util=set_subname -E 'my $c = sub() {die}; set_subname have_name => $c; $c->()'
  • by cherryteastain on 6/15/25, 7:22 PM

    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.
  • by mandarax8 on 6/15/25, 8:24 PM

    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...

  • by not-so-darkstar on 6/15/25, 6:31 PM

  • by petters on 6/16/25, 6:51 AM

    > In fact, I don’t think anyone understands std::function<> including the 3 people who implemented it.

    "I don't understand it, so surely it must be very difficult and probably nobody understands it"

  • by CraftingLinks on 6/16/25, 5:22 AM

    "Templated code is a highway to bloat." Should be on a t-shirt.
  • by jitans on 6/15/25, 6:58 PM

  • by pif on 6/16/25, 9:18 AM

    > I don’t understand std::function<> implementation.

    This is the kind of (maybe brilliant, maybe great, maybe both, surely more than myself) developers I don't like to work with.

    You are not required to understand the implementation: you are only required to fully understand the contract. I hate those colleagues who waste my time during reviews because they need to delve deeply into properly-named functions before coming back to the subject at hand.

    Implementations are organized at different logical level for a reason. If you are not able to reason at a fixed level, I don't like to work with you (and I understand you will not like to work with me).

  • by mgaunard on 6/15/25, 9:58 PM

    Should have just implemented his own std::function with the simplicity and performance trade-off he wanted.
  • by AlienRobot on 6/16/25, 6:45 PM

    >The implementation cleverness: use a special, impossible value of a pointer (-1) to indicate a function without arguments.

    From what I know about C this code probably breaks on platforms that nobody uses.

    Thanks for Sumatra, by the way :D Very useful software!

  • by tempodox on 6/16/25, 2:33 PM

    Slightly off-topic: Thanks to the author for SumatraPDF! It's an excellent Windows app that saves me (and many others, I'm sure) from having to use that horrible shit show that is Acrobat Reader.
  • by b0a04gl on 6/16/25, 6:24 PM

    the approach works, but there's hidden cost in how it shapes the compiled output. every callback adds a layer the compiler has to guess around. curious if anyone checked what this does to inlining and branch prediction across builds. does the extra indirection prevent useful optimisations? or does the compiler end up being too aggressive and misoptimise when the struct layout changes later? would be useful to diff the assembly across releases and compilers
  • by _ZeD_ on 6/16/25, 8:26 AM

    > In programming language lingo, code + data combo is called a closure.

    in my day code + data was called a class :)

    (yeah, yeah, I know closure and class may be viewed as the same thing, and I know the Qc Na koan)

  • by commandersaki on 6/15/25, 9:29 PM

    I always love this author's writing style, his articles are pure bliss to read.
  • by account42 on 6/16/25, 11:50 AM

    "Surely no one will ever need to pass -1 as user data"

    Why place undefined behavior traps like that in your code.

  • by agent327 on 6/16/25, 7:32 AM

    It's unfortunate that the author hasn't spent some time figuring out how to get a stack trace, that would have saved him from reinventing std::function badly.

    Also, sad to see people still using new. C++11 was 14 years ago, for crying out loud...

  • by pjmlp on 6/15/25, 8:09 PM

    Another example of NIH, better served by using the standard library.
  • by mdaniel on 6/15/25, 7:01 PM

    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?