Can you elaborate on your third point? What would a class need to do to affect debugging info?
Regarding your fourth point, sometimes an architecture can be vastly simplified if the source of information can abstracted away. For example, invoking a callback from a TCP client, batch replay service, unit test, etc. Sometimes object oriented design gets in the way.
To your first point, I think RAII and architecture primarily address this. I'm not sure that I see callback implementation driving this. Although I have seen cancellable callbacks, allowing the receiver to safely cancel a callback when it goes away.
>> Can you elaborate on your third point? What would a class need to do to affect debugging info?
Common implementations are a function pointer + void* pair, which in most debuggers just show you two opaque addresses. Better to include a info block -- at least in debug builds -- with polymorphic type pointers that can actually deduce the type and show you all the fields of the receiver.
>> sometimes an architecture can be vastly simplified if the source of information can abstracted away.
"sometimes" is doing a lot of heavy lifting here. That's my whole point -- more often than not I see some type of homespun functor used in cases that are _not_ simplified, but actually complicated by the unnecessary "plumbing."
>> RAII and architecture primarily address this
If the receiver uses RAII to clean up the callback, then you've reintroduced the "type-intrusiveness" that functors are meant to avoid...?
> most debuggers just show you two opaque addresses
This has not been my experience. But I haven't needed to deal with RTTI disabled.
By RAII, I mean using destructors to unregister a callback. This covers 99.9% of use cases. Generally callback registration is not where you really want type erasure anyways.
>> By RAII, I mean using destructors to unregister a callback.
_Whose_ destructor, if not the receiving-type? Is there a third "binding" object, because then you have three potentially-unrelated lifetimes.
>> Generally callback registration is not where you really want type erasure anyways.
I'm responding to the article: "Some mechanisms for doing callbacks require a modification to, or derivation of, the caller or callee types. The fact that an object is connected to another object in a particular application often has nothing to do with its type. As we'll see below, mechanisms that are type intrusive can reduce the flexibility and increase the complexity of application code. "
The receiving type should control the lifetime of any callbacks to itself that it gives away. The destructor is the best place to ensure this gets properly cleaned up.
Like anything, custom
callbacks can be used well or misused. Design is a matter of expertise and taste bordering on an art form. Connecting framework implementation and business logic can be done cleanly or clumsily. I am skeptical of an argument that callbacks have a code smell prima facie.
I don't disagree, but the article does. RH describes an architecture where setup functions create callbacks, independent of the receiving type. If I were to steelman him, it would be something like this: "in a pedantic MVC system, model objects don't depend on view objects by design, and therefore should not be aware that their methods are used as 'click callbacks'"
C++ was so much cleaner in the 90s, when it was still essentially "C with classes," which is how I like to use the language. Modern standards have turned it into an ugly mess.
Amen. The syntax just kept getting more and more complicated. I gave up in the late 1990s. Ironically for this post, I now prefer to write everything in Clojure. It seems like my own journey has paralleled Rich’s journey. Maybe that’s why I appreciate so many of the design choices in Clojure. It’s not perfect, but it’s really, really good.
And then they introduced coke at committee meetings, the crazy shit they've been coming up with lately shows absolutely zero understanding of the complexity issue.
and yet client code can be incredibly simpler nowadays thanks to all these features.
I can write this simple struct:
struct Person {
std::string name;
my_complicated_date_type date_of_birth;
};
and get serialization, network interop, logging, automated UI generation, hashing, type-safe IDs, etc. without having to write an additional line of code. Twenty years ago you had to write 2000 lines of additional boilerplate for each data type to get to the same place.
As some not into modern c++, how would you get all functionality? Is there any guide or documentation yout could point to?. It is fascinating if all that could be done with just the struct defenition.
I also use C++ as "C with classes," however I will concede that many of the modern C++ additions, particularly around templating, are extremely convenient. If you haven't had a chance to use requires, concepts, "using" aliases, etc I'd recommend giving them a try. I don't reach for those tools often, but when I do, they're way nicer than whatever this article is demonstrating from 1994! Oh yeah, also lambdas, those are awesome.
I dunno, I skimmed the article's 31 year old code examples and immediately thought they would be shorter and simpler in c++11 or later.
But it's important to see the 1994 (and 1998) view of the world to understand how modern c++ features work. Because they start from that worldview and start adding convenient stuff. If you don't understand how c++ used to work, you may be confused with why c++ lambdas look so weird.
This is a thing C++ advocates say that tells me they’ve never really tried to do it and share that codebase with others or integrate with other codebases.
You generally don’t get to pick what parts other people want to use, which means that in the end you still have to deal with the entirety of the language.
Even when working alone, the complexity gradually creeps up on you.
Because it's all made to work together, start pulling anywhere and before you know it you're using another feature, and another, and so on.
And many features interact in exotic and hard to predict ways, so hard that entire careers have been spent on trying and failing to master the language.
I didn't want to use the functional part of C++, then one day my colleague with a twisted sense of humour checked in fun.hpp with his own implementation of FP.
Boom.
Now you are not only using the functional part of C++, but also in a nonstandard way! Merci Gilles. :)
not sure why the title was renamed, but i thought this was interesting primarily because it's the early work of Rich Hickey, famous for making the Clojure language.
Doing this today I'd just have a std::function parameter and have callers pass in a lambda. I may use a third party std::function that doesn't have the weird copy semantics though
> I may use a third party std::function that doesn't have the weird copy semantics though
Note that C++23 brings std::move_only_function if you're storing a callback for later use, as well as std::function_ref if you don't need lifetime extension.
Red flags for me when I see nonstandard functors in a c++ codebase (esp if the "glue" is in a setup function independent of the objects):
(i) Have they thought about the relative lifetimes of the sender and receiver?
(ii) Is the callback a "critical section" where certain side-effects have undefined behavior?
(iii) Does the functors store debugging info that .natvis can use?
(iv) Is it reeeeeeeally that bad to just implement an interface?
> Red flags for me when I see nonstandard functors in a c++ codebase
Even if it's 1994???
Yes in 1994 I had these exact judgements, at age 11 :P
Clever kid
Can you elaborate on your third point? What would a class need to do to affect debugging info?
Regarding your fourth point, sometimes an architecture can be vastly simplified if the source of information can abstracted away. For example, invoking a callback from a TCP client, batch replay service, unit test, etc. Sometimes object oriented design gets in the way.
To your first point, I think RAII and architecture primarily address this. I'm not sure that I see callback implementation driving this. Although I have seen cancellable callbacks, allowing the receiver to safely cancel a callback when it goes away.
>> Can you elaborate on your third point? What would a class need to do to affect debugging info?
Common implementations are a function pointer + void* pair, which in most debuggers just show you two opaque addresses. Better to include a info block -- at least in debug builds -- with polymorphic type pointers that can actually deduce the type and show you all the fields of the receiver.
>> sometimes an architecture can be vastly simplified if the source of information can abstracted away.
"sometimes" is doing a lot of heavy lifting here. That's my whole point -- more often than not I see some type of homespun functor used in cases that are _not_ simplified, but actually complicated by the unnecessary "plumbing."
>> RAII and architecture primarily address this
If the receiver uses RAII to clean up the callback, then you've reintroduced the "type-intrusiveness" that functors are meant to avoid...?
> most debuggers just show you two opaque addresses
This has not been my experience. But I haven't needed to deal with RTTI disabled.
By RAII, I mean using destructors to unregister a callback. This covers 99.9% of use cases. Generally callback registration is not where you really want type erasure anyways.
>> By RAII, I mean using destructors to unregister a callback.
_Whose_ destructor, if not the receiving-type? Is there a third "binding" object, because then you have three potentially-unrelated lifetimes.
>> Generally callback registration is not where you really want type erasure anyways.
I'm responding to the article: "Some mechanisms for doing callbacks require a modification to, or derivation of, the caller or callee types. The fact that an object is connected to another object in a particular application often has nothing to do with its type. As we'll see below, mechanisms that are type intrusive can reduce the flexibility and increase the complexity of application code. "
> Whose_ destructor, if not the receiving-type
The receiving type should control the lifetime of any callbacks to itself that it gives away. The destructor is the best place to ensure this gets properly cleaned up.
Like anything, custom callbacks can be used well or misused. Design is a matter of expertise and taste bordering on an art form. Connecting framework implementation and business logic can be done cleanly or clumsily. I am skeptical of an argument that callbacks have a code smell prima facie.
I don't disagree, but the article does. RH describes an architecture where setup functions create callbacks, independent of the receiving type. If I were to steelman him, it would be something like this: "in a pedantic MVC system, model objects don't depend on view objects by design, and therefore should not be aware that their methods are used as 'click callbacks'"
Heard about this watching Casey Muratori's "The Big OOPs" talk [0]. Thought it couldn't be _that_ Hickey, but turns out it was!
[0] https://youtu.be/wo84LFzx5nI?si=SBv1UqgtKJ1BH3Cw&t=5159
Related. Others?
Callbacks in C++ using template functors (1994) - https://news.ycombinator.com/item?id=18650902 - Dec 2018 (50 comments)
Callbacks in C++ using template functors – Rich Hickey (1994) - https://news.ycombinator.com/item?id=12401400 - Aug 2016 (1 comment)
Callbacks in C++ using template functors (1994) - https://news.ycombinator.com/item?id=10410864 - Oct 2015 (2 comments)
C++ was so much cleaner in the 90s, when it was still essentially "C with classes," which is how I like to use the language. Modern standards have turned it into an ugly mess.
Amen. The syntax just kept getting more and more complicated. I gave up in the late 1990s. Ironically for this post, I now prefer to write everything in Clojure. It seems like my own journey has paralleled Rich’s journey. Maybe that’s why I appreciate so many of the design choices in Clojure. It’s not perfect, but it’s really, really good.
A sentence from the article: "Given the extreme undesirability of any new language features I'd hardly propose bound-pointers now."
It shows that C++ was considered too complex already in the 90s.
And then they introduced coke at committee meetings, the crazy shit they've been coming up with lately shows absolutely zero understanding of the complexity issue.
and yet client code can be incredibly simpler nowadays thanks to all these features.
I can write this simple struct:
and get serialization, network interop, logging, automated UI generation, hashing, type-safe IDs, etc. without having to write an additional line of code. Twenty years ago you had to write 2000 lines of additional boilerplate for each data type to get to the same place.As some not into modern c++, how would you get all functionality? Is there any guide or documentation yout could point to?. It is fascinating if all that could be done with just the struct defenition.
You would have added some codegen, should be possible to write a codegen framework from scratch in 2000 lines even.
Arguably the result would have been easier to read and maintain and not as slow to compile.
Or you might have used table based data structures, like TeX and the lunar lander did.
You do the best you can, today, with what you have, and then you ship it and get on to the next challenge. Same now as it always was.
I also use C++ as "C with classes," however I will concede that many of the modern C++ additions, particularly around templating, are extremely convenient. If you haven't had a chance to use requires, concepts, "using" aliases, etc I'd recommend giving them a try. I don't reach for those tools often, but when I do, they're way nicer than whatever this article is demonstrating from 1994! Oh yeah, also lambdas, those are awesome.
I dunno, I skimmed the article's 31 year old code examples and immediately thought they would be shorter and simpler in c++11 or later.
But it's important to see the 1994 (and 1998) view of the world to understand how modern c++ features work. Because they start from that worldview and start adding convenient stuff. If you don't understand how c++ used to work, you may be confused with why c++ lambdas look so weird.
>ugly mess
That may be the case, but there are plenty of examples of elegant implementations.
JUCE, for instance:
.. I think that's kind of clean and readable, but ymmv, I guess?Well, that definitely doesn't look "clean and readable" to me for whatever that's worth.
Too many []’s and ::’s for your eyes?
No member function templates, no variadic templates, no std::function, no lambdas, etc. That's certainly not the kind of C++ I would want to write...
You can just use the parts you want though; that's part of its appeal.
This is a thing C++ advocates say that tells me they’ve never really tried to do it and share that codebase with others or integrate with other codebases.
You generally don’t get to pick what parts other people want to use, which means that in the end you still have to deal with the entirety of the language.
Exactly, it doesn't work very well in practice.
Even when working alone, the complexity gradually creeps up on you.
Because it's all made to work together, start pulling anywhere and before you know it you're using another feature, and another, and so on.
And many features interact in exotic and hard to predict ways, so hard that entire careers have been spent on trying and failing to master the language.
I didn't want to use the functional part of C++, then one day my colleague with a twisted sense of humour checked in fun.hpp with his own implementation of FP.
Boom.
Now you are not only using the functional part of C++, but also in a nonstandard way! Merci Gilles. :)
not sure why the title was renamed, but i thought this was interesting primarily because it's the early work of Rich Hickey, famous for making the Clojure language.
Doing this today I'd just have a std::function parameter and have callers pass in a lambda. I may use a third party std::function that doesn't have the weird copy semantics though
> I may use a third party std::function that doesn't have the weird copy semantics though
Note that C++23 brings std::move_only_function if you're storing a callback for later use, as well as std::function_ref if you don't need lifetime extension.
https://en.cppreference.com/w/cpp/utility/functional/move_on...
This paper is about the idea that eventually became std::function.