Don't know about the code subtilities, but SumatraPDF is a gift for viewing PDF on MS Windows.
So big thanks to the author !
mwkaufma · 9m ago
The lengths some go to avoid just using a bog-standard virtual function.
_randyr · 2h ago
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:
Perhaps I'm mistaken in what the author is trying to accomplish though?
OskarS · 1h ago
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 · 1h ago
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.
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 · 2h ago
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.
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
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 · 1h ago
> 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++.
spacechild1 · 1h ago
> 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 · 1h ago
std::bind is bad for him for the same reasons std::function is bad though
spacechild1 · 1h ago
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 · 1h ago
Does a stack trace from a crash in a bound function show the line number of where the bind() took place?
spacechild1 · 56m ago
No, but neither does the author's solution.
delusional · 1h ago
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.
commandersaki · 27m ago
I always love this author's writing style, his articles are pure bliss to read.
mandarax8 · 1h ago
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.
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.
mandarax8 · 22m ago
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.
spacechild1 · 1h ago
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 · 36m ago
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 · 24m ago
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. You need to always heap allocated your data. 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?
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 · 3h ago
A small kitten dies every time C++ is used like its 1995.
void (*fn)(void*, T) = nullptr;
tom_ · 21m ago
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.
plq · 2h ago
Unless you mutter the magic incantation "C compatibility" while doing it
zabzonk · 2h ago
did nullptr exist in c++ back in 1995 - i can't remember
trealira · 2h ago
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.
noomen · 2h ago
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.
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".
pjmlp · 1h ago
Another example of NIH, better served by using the standard library.
mdaniel · 2h ago
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 · 2h ago
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 · 2h ago
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
kjksf · 2h ago
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 · 2h ago
sumatrapdf not being cross platform is a great feature, maximizing the use of intended platform. win32api is great. thank you for that
mhd · 2h ago
> 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++.
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<>`.
> 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.
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.
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++.
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.
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.
https://github.com/TartanLlama/function_ref/blob/master/incl...
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.
Why do you even care how std::function is implemented? (Unless you are working in very performance critical or otherwise restricted environments.)
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.
BTW, I would also contest that your version is faster at runtime. You need to always heap allocated your data. 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?
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.
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".
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.
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
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.
Probably by using a cross-platform toolkit written in C++.