Don't know about the code subtilities, but SumatraPDF is a gift for viewing PDF on MS Windows. So big thanks to the author !
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)
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?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.
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.)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
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.
> 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.
The lengths some go to avoid just using a bog-standard virtual function.
A small kitten dies every time C++ is used like its 1995.
void (*fn)(void*, T) = nullptr;
> [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->()'
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.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...
> 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"
"Templated code is a highway to bloat." Should be on a t-shirt.
Why don't use fu2? https://naios.github.io/function2/
> 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).
Should have just implemented his own std::function with the simplicity and performance trade-off he wanted.
>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!
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.
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
> 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)
I always love this author's writing style, his articles are pure bliss to read.
"Surely no one will ever need to pass -1 as user data"
Why place undefined behavior traps like that in your code.
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...
Another example of NIH, better served by using the standard library.
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?
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".