The amusing thing is that C++ still doesn't have nested functions. (Although I think that was a GCC feature at one point.)
As I point out now and then, there are two basic concepts here - closures and anonymous functions. A lambda is both. All four combinations (functions with/without closure, and with/without names) are possible. There's a tendency to confuse the two. An anonymous function is syntactical sugar. A closure is a run-time construct.
- Python and Javascript have nested functions with inheritance, which are closures. Most garbage collected languages have that, because it's easy to implement. Python also has lambdas, which von Rossum didn't want to put in but did to shut up the crowd that wanted them.
- Rust has nested functions without inheritance. A nested function can't access its outer scope. Rust also has lambdas, and they can inherit. C++ has now gone the same route. Non-GC languages need compile-time work to implement a closure, since something has to make a copy of what's inherited and worry about its lifetime.
- It's possible to have anonymous functions without inheritance. The classic example is a sort key function, where a sort function is called with a function that defines the test.
Are D's nested functions truly functions - for example are they ABI-compatible with a top-level function? If so, how do they get access to the stack frame?
gcc has this feature, it is implemented by constructing a trampoline-style function on the stack. This requires an executable stack, so isn't really a thing any more.
> for example are they ABI-compatible with a top-level function?
If they are annotated with `static`, yes. If they are not so annotated, they are ABI-compatible with non-static member functions. In fact, in D, a pointer to a non-static member is interchangeable with a pointer to a non-static nested function. This makes for much convenience when dealing with algorithms that accept such pointers (called "delegates" in D):
> how do they get access to the stack frame?
And extra argument is passed to the function which is the frame pointer to the calling function. (This is called a "static link". The "dynamic link" is the return address.) This can be thought of as the "this" pointer to the "struct" that represents the caller's stack frame, hence the binary compatibility with member functions.
To go back to the enclosing enclosing enclosing function's stack frame, you dereference the static link 3 times.
The tricky part is optimizing these dereferences away when there are no captured variables in a stack frame.
D has native support for 'delegates'; closures, essentially. Function literals that reference objects from an outer scope are delegates, and ABI-compatible with other delegates. Regular function pointers can trivially be converted to delegates, so if you want to store function pointers, you should store delegates instead and convert transparently as needed. You can't pass delegates into C functions (because they're only in the D abi, not the C abi), though most C libraries that take a callback also take a 'state' pointer that you can use instead.
Tl;dr: it's a bit complicated, especially wrt communication with c libraries, but in general they're interchangeable with other functions.
Lambdas are (among other things) nested functions. The "tendency to confuse the two" is a feature, not a bug.
Bob knows how to draw quadrilaterals. He uses this skill to draw rhombuses, rectangles, and parallelograms. IMHO you're arguing that Bob doesn't know how to draw rhombuses because the method he uses to draw rhombuses is a method to draw parallelograms.
A nested procedure ("function" is a bit misleading in imperative languages) can close over variables in the outer scope, but has to have its own isolated control.
A truly[2] anonymous procedure can also close over control flow:
fun hasZeros(ints: List<Int>): Boolean {
ints.forEach {
if (it == 0) return true // returns from hasZeros
}
return false
}
[2]: "truly" in the sense it's never assigned to a name, and is known to not escape its surrounding scope, which is necessary if the compiler is going to allow it to, e.g. do a non-local return.
Can you provide a ... slightly more real world example of what non-local returns are for? As described it sounds equally capable of shooting both varmints and one's own feet.
In C++, if you leave the capture specifier at the beginning of a lambda expression empty (e.g. [](int x) { return x + 1; }), you can't capture variables from outer scopes, so you effectively have an anonymous function without inheritance. Non-capturing lambdas can be coerced to regular function pointers, so they behave almost identically to true functions, though they technically have different types (before being coerced).
Rust also allows the "non-capturing lambda to function pointer" conversion but (for better or worse) doesn't have explicit syntax for non-capturing lambdas; a lambda automatically qualifies if it doesn't mention any outer variable names.
I thought the difference is more in how the runtime is structured, i.e. a closure has the ability to make use of the scope in which it's defined in, so that it can make use of variables and functions within it when it's actually called.
I was a C/C++ programmer from 1995 to 2012, almost 20 years. I moved on to more SaaS-friendly languages, but when I look at C++ now, it's incomprehensible to me. It's an entirely different language. I think I could get back into it, but it's been so long and the gap between where I left it and where it is now is so vast, coupled with the dearth of jobs compared to Python or Go, that it doesn't feel worth it anymore. It's weird knowing that a language that I spend so much time on is relegated to the past.
> I was a C/C++ programmer (...) to 2012 (...) when I look at C++ now, it's incomprehensible to me.
Understandably so. You've used C++ during a time where C++98 reigned and stagnated C++, and left just when that long-standing stagnation finally finished.
Since then 4 new C++ standards have been published. Smart pointers are in. Move semantics are in. Lambdas are in. Auto type deduction is in. Hell, range-based for loops are in.
Your timing was unfortunate. I'm sure programmers who were used to Fortran77 also have problems reading Fortran95 code.
Yep, exactly this. It was only 8 years ago, but its several generations of changes for C++. As you said, it was very unfortunate timing, but not much I can do except pick up a book and start learning. But I'd probably dedicate that energy to delving deeper into Python, Go or even Java at this point. Sad to see decades of work no longer relevant, but it got me to where I am today, so it was worth something I suppose.
I have a love/hate relationship with C++, when programming python I really miss the strong typing and other compile time checks, not to mention the performance.
Other times I hate it especially templating compile errors.
I've been out of the game for a while, so I could be way off here, but my experience during my brief (15-20 year) career was one could always get paid significantly more for doing C++ than Python.
Which makes intuitive sense to me since it's a lot harder to master C++.
This isn't true. Go/Python/non-C++ can get you dozens of jobs with very high total comp, well over anything for C++. A Senior Software Engineer at any FANG will get you over 400k/yr TC. Getting an equivalent job with C++ would be much harder.
You misunderstand me: I'm not claiming its "mostly C++", I'm claiming that its "mostly C++ and Java". Python isn't zero, but its very far down the list, both in lines of code and machine cycles spent.
Certainly, although you can work there using almost entirely python (I did for two years, although I'm now having to use more and more c++ and java as my work shifts to be infra that involves products instead of just raw infra)
I was under the impression that one would need C++ to get one of those 400k+ FAANG jobs. Obviously, I could be wrong.
I've never worked at one myself, but they do hit me up from time to time, and seem to be mostly interested in my C++ experience.
As I said in my disclaimer, my actual experience is very dated at this point, but a strong C++ developer could earn 400k+ even way back in 2011 in the NYC financial industry. Back then, I don't think I'd heard of any Python developers making much more than 150k.
Nowadays, the people I know working in C++ in NYC finance are all making significantly more than 400k.
I sympathize with you here. I kind of like the lambda syntax for simple cases in C++, but the complex cases look painful to me, and based on everything else in C++, I predict there are many special cases to remember.
However, if you just want a more conventional syntax for nested functions, you can almost get what you want by placing a static method inside of a local class, and it's available in older versions of C++ too:
If you want it to "close" over your local variables, you'll probably need to actually instantiate an instance of the local class and use a non-static method. Doing this, the capture lists in lambdas almost start to make sense.
Clever. If you let the nesting function have static variables they can be referenced from the nested function too, but actually using lambdas would of course be more clear in practice.
The only feature of lambdas that a proper nested function would miss is the "copy capture".
> The only feature of lambdas that a proper nested function would miss is the "copy capture".
Yeah, reference by default would fit with what people probably expect from other languages, but without a garbage collector (or similar) that's only valid for downward passed functions. Copying allows your nested function to escape the lifetime of the calling function, which might require some other/additional syntax.
Not sure I like implicit capture more than explicit capture but there's [=] if you need it. Also, the return value as the article mentions is optional, so most lambdas look like
const auto nestedfunc = [=](a,b) { /* */ };
Which isn't that much different to me than
{ /* some scope */
function nestedfunc(a,b) { /* */ }
}
const and auto are needed but because it's C++. Beyond that, you have an equals sign which is needed here but [=] which is three characters. Again, small differences, not sure why it looks that much worse.
Now that we know lambdas they are fine, but to answer the question "why is there a book" in the beginning of the thread, it is more or less because it is not as obvious as gcc's c extension.
why [&args...] and not [args...] ? do you really want to capture by reference when you're setting a lambda as a callback ? (likewise in the other direction - tons of arguments for every default, already debated ad nauseam)
I don’t know any other language with lambdas which requires me to spell out explicitly which variables it should close over... so you tell me how Java, Scala, Clojure, Kotlin, Python, JavaScript, Rust, Go, C# and tens of others managed this clearly impossible feat of engineering.
Rust is not really the same as C++ here, you get "the compiler infers how to capture things" as the default behavior, and "take everything by value", which happens with the "move" keyword. We've found that these two cases cover the vast, vast, vast majority of usage, and so C++-style specific capture modifiers aren't necessary. You can emulate C++ style stuff by taking references outside of the closure and using move to capture only the reference.
I'm only mildly familiar with C++, but the support for template arguments in lambdas looks like an analogue for generic closures, which are absent from Rust, but would be really handy at times.
So that ForwardToTestFunc right? At what time are the template arguments filled in? Could you call it first with an int and then later with a string, each call forwarding to a different overload?
The more I program in Go, the more retroactively irritated I get at C++. A buddy of mine did a few 'brown bag' sessions on advanced C++ templating and my jaw just dropped further and further at each one. Who has the energy for this? It's hard to write, it's hard to read, it's hard to debug.
For someone who a) knows what they're doing and b) really needs to squeeze every last millisecond out of the code I suppose they could be useful. It made me want to send Rob Pike a thank you card.
IMHO if you're messing with templates in C++ you've probably over-engineered your solution.
There are a few cases where they can make sense, like in the standard library, but most of the time without them your code will be cleaner and you'll never actually run into the case where they pay off.
I've run into cases where templates pay off once or twice. I was once able to refactor 1000+ lines of code into about 150 lines using a template class. However, I've tried more than once to learn "advanced" meta-programming techniques, and I've yet to find a use case that makes any sense.
I wrote a library a few years back to do in-place non-square transposition in the GPU register file. The algorithm had a lot of math that needed to be computed for every size matrix. I wrote it in C++03, before any constexpr, so the metaprogramming is intense. One of these days I'll rewrite it in C++20 and most of the code will go away. This code is still in use by the way.
If you need compile-time stuff that constexpr can't do, then you have little choice. Of course, in reality, its very rare that you "need" compile-time stuff.
I wasn’t referring to genetics or saying that templates aren’t necessary or useful, or even that they aren’t common. Those uses of templates are relatively straightforward most of the time. Where templates, IMHO, become unwieldy is when trying to compute things at compile time. In many cases constexpr works here, but my point was that the remaining cases are relatively rare, certainly when you need it (as opposed to just wanting it, eg for performance).
> A buddy of mine did a few 'brown bag' sessions on advanced C++ templating (...) Who has the energy for this? It's hard to write, it's hard to read, it's hard to debug.
C++ programmers who dedicate themselves to template metaprogramming are like stunt performers. You see them get on their shiny bikes and flashy costumes, you clinch your fists in a mix of fear and awe when they showcase their skills, but in the end it might be entertaining but it has virtually no real practical use beyond extremely niche applications.
I'm afraid you've describe my friend to a T. Ah well, it takes all types. If we couldn't work with some strange personalities, we wouldn't make it far in this business.
C++ templates can indeed get crazy, but I miss them in every other language I use. For example, I recently wrote some code in which I needed to know whether or not a member existed in a class, at compile time. Turns out, there's a way to do that in C++ using templates and overload rules:
Yes, conceptually it's a bit nuts, but the fact that you can do stuff like this makes it unlikely that you'll ever get completely stuck trying to implement something.
I personally love C++ because it's just a giant bag of tools, even if one of those tools is a footgun.
Having experienced similar "ah thank god C++ allows me to do [ridiculous template magic]" moments, 99% of the time it is because the language is forcing you (or leading you) into some kind of design that would have never been necessary in a different language. I have seen many hours/days wasted on using esoteric C++ features in order to get around a language limitation/quirk.
So I am going out on a limb here and assuming that your needing to know whether or not a member exists in a class at compile time is simply something you would never need in a different language because the language would allow you to design for something simpler that satisfies the same requirements. As someone who writes both C++ and C# daily, I often end up comparing the two and most of the time I end up thinking "it's cool that you can do that in C++ but this would have been half the lines and cleaner code in C#".
In my case, I needed to know whether or not a member exists in order to implement object tracking. I basically have a macro that inserts a member into a class, and calls that member's constructor with the "this" pointer of the tracked class. A different function needs to know, at compile time, whether or not a class is tracked, as the way memory is allocated differs.
I didn't want to use inheritance, as tracking is disabled in a shipped build, and I want it to have as little effect on the codebase as possible. So, the simplest option seemed to be to check whether or not the member exists (and assume the name is unique enough an untracked object will not contain it). This was good enough for my purposes.
I'm not as familiar with C#, but attributes seems like a potential alternative, though I'm not sure they can be fully removed from a release build. And, of course, C# is garbage collected, which is not ideal for all types of software. If there is a better alternative, I'd be interested in hearing it though!
Don't get me wrong, I'd absolutely love a "better" C++. Rust is a great language, but I personally don't feel that the safety guarantees are worth it for every class of software. If a video game crashes, the world isn't going to end.
Agreed, it is lacking tooling support and many other things, but it is a cleaner C++. That plus the borrow checker are its two major features. Otherwise, nobody would use it and their designers would have done a pretty bad job given the 30 years of experience they had from C++!
I sometimes feel like sending one to Bjarne Stroustrup too, because C++ is still an incredible language. I see Rob and Ken talking about how they hate C++ and I worry about poor Bjarne's feelings.
Well, features like exceptions and classes don't improve performance. You could write the same code as fast in C, even if it would perhaps be more cumbersome and error prone to read and write. You save perhaps development and maintenance cost, but not runtime cost. Features like lambdas don't improve performance either, they rather make the language easier to write and even more hard to read (think of the name of a function as part of the documentation).
In theory the higher abstraction of C++ provides for optimization opportunities for a sufficiently smart compiler. I have yet to see such though.
Write the little bits which need to be fast in C and the gross in a sane language mere mortals can read (e.g. no 'most vexing parse').
I've programmed in C++ off and on for the last 25 years. I keep wondering if I should spend the time to level up on modern C++ (C++11,14,17,20) or if I might be better off to just chuck it all and dive into Rust and/or Zig and place my bets there instead.
I did this a few years ago: picked up C++ 17 after a 25 year hiatus by treating it as a brand new language (and by treating its connection to C as you would Java’s connection to C: a distant ancestral relative).
Modern c++ is a powerful, expressive language. It’s not a “object oriented” language in the fetishistic sense — I don’t write that many classes comparatively. A lot of generic programming and functional algorithms in my case. And I can couple directly to the iron as needed.
Yes there are some unfortunate syntactic issues due to it having evolved from C but that’s how the world works.
Does anyone actually use Zig in production? I've only ever read about it here on HN.
There's no avoiding Rust. It's becoming the jQuery of systems programming.
C++ has lost the plot in some respects but you don't, and probably won't ever, end up using all of it. A lot of it is to make it easier to write things like the STL, not your typical end-user program. Learn up to 17, there's some good stuff in there. If someone in an interview gives you some highly complex templatey code snippet to analyse just tell them to go fuck themselves.
I agree that C++ has lost the plot, but I find that it's a very good language for code generation. I don't write it directly, but I use its incredible compilers and ecosystem, including multiple profiling and debugging tools (uftrace is a nice one that even dynamic languages seem to lack).
I'm translating a subset of statically typed Python to C++:
I use very much a "pidgin" subset of the language -- one that is easy to step through in the debugger! The problem with all these fancy new features is that you're more likely to trip over bugs in the debugger, e.g. incorrect line number information and stack traces.
So the generated code doesn't even use operator overloading, which e.g. C++ 11 range for loops require. I want to be able to see everything. Yet I also want to use features like templates, virtual methods, and strict enums, which C doesn't have.
-----
Another "modern" project that does this is the Souffle datalog compiler, which generates "monolithic" C++ programs with heavy use of C++ templates:
(I heard about it due to its role in prototyping Rust's type system)
C++ templates are not fun to write but they have some interesting properties for code generation.
-----
So I would happy if we stopped writing C++ by hand. But I do not like the alternative rewriting inferior versions of existing programs/libraries in entirely new languages. They often have new bugs, are slower, and have fewer features.
I really like what Zig is doing to reuse existing code (i.e. bundling a C compiler). I think that both Rust and Go ecosystems have led to a lot of fragmentation and redundant efforts.
Likely not, or at least they shouldn't, as it still has yet to have a 1.0 release and is undergoing constant changes in syntax and API.
However, that doesn't mean one can't take the time to learn it right now. Zig shows a lot of promise for being a real contender in this space once it does have a stable release.
Zig is cool but still very much unstable, running it in production is very much not recommended and/or insane. There's still quite a lot of compiler/stdlib bugs, design edge cases, and breaking changes including introduction of new bugs.
> There's no avoiding Rust. It's becoming the jQuery of systems programming.
I'm not sure if that's meant as a good thing or a bad thing. Given the target audience, I expect a good thing, as for people not entirely immersed into JavaScript and web development at the time (which I expect most people reading about C++ features here were not), jQuery was a godsend. It gets kind of a bad wrap these days, but from my perspective, JavaScript just adopted all the paradigms it encouraged into the language so it's no longer needed. Hard to argue with that success, IMO.
jQuery was undermined not because it wasn't a good idea or it was poorly designed, but because it matured early on and Javascript evolved rapidly enough that the technical premises it was built on no longer applied.
A more concrete example of this is Windows and Java's use of UCS-2 as their character type. That was a forward thinking move which then became a painful legacy issue when Unicode changed.
That's inevitably going to happen to Rust to some extent, but it's targetting a realm that is far slower to evolve and it seems like they are very mindful of history and theory.
I keep wondering if I should spend the time to level up on modern C++ (C++11,14,17,20)
Yes, absolutely. I (somewhat recently) did that, and I find modern C++ to be almost like a new language. The "modern" features are very useful and powerful.
Go with Golang if you want simplicity. Go with Rust if you want a complex but still neat programming language. Rust is much better than C++ in my humble opinion. I use these programming language as a hobby and I don't think I will ever use c++ on a new project again.
I've never been that impressed with Go - it seems to be missing a lot of modern features. To me, Zig looks nice from a simplicity standpoint and it fits in the systems programming space.
I don’t mean this as an insult: Pike is quite open about choosing a different region of the design space; he had good reasons for it, they just happen not to work for me.
Learning up to C++20 if you are an experienced C++ programmer is easy as long as you only use whatever features you learn or make sense for your project.
Studying everything new is, of course, a much more ambitious goal.
If you learn Rust or C++20, the marginal cost of learning the other should be trivial.
After writing a bunch of Rust, I recently had to learn C++. I've been storing C++ in my brain as "Rust + delta of dubious ideas", which, not to start a flame war, has easily been a better compression algorithm for my brain than storing C++ in isolation.
For example:
- C++, const is part of the type. Clearly wrong.
- Rust, mut is property of variable, or & vs &mut. Correct, but lack of & with mut-polymorphism just causes needless suffering.
- Rust construction: good, Rust destruction: bad
- C++ destruction: bad, basically like Rust's. C++ construction: bad, matches C++ destruction.
(I've been saying for years that drop needs to consume, not borrow the thing to be destroyed, but no uptake :(.)
It’s pretty hard to express that an object must be invariant without a thing like that. You can’t do so with the storage itself as the object may be passed to a function.
But if something is passed by value, the caller should never care whether the callee mutates its (shallow) copy. It's purely an unobervable implementation detail of the function.
`drop(&mut self)` means, strictly going by the types I could call drop twice! We want an `&own` that is like C++'s rvalue references (so we can still drop DSTs) so we can have `drop(&own self)`.
This correctly prevents one from calling Drop::drop twice without extra hacks, and demonstrates that the location is completely deinitialized and can be reinitialized to anything.
It is also dual to how, seemingly magically,
let x;
x = foo();
works without Rust thinking there is an old value of `x` to get rid of.
> `drop(&mut self)` means, strictly going by the types I could call drop twice!
Wouldn't calling a destructor on an object twice will also compile in C++? I don't really see this as an issue in either language; the solution is "don't ever call `drop`/destructors manually"
Other languages manage to be parsable without the syntactic aesthic getting Lovecraftian. I think the other factor in C++ is the how much has been added to the language while maintaining backward-compatibility.
It's new feature for a standard that's not even out yet. It doesn't matter how long have you been learning C++, unless you are invested in the language design and read proposals.
One of the worst design decisions in C++ (IMO, of course) is the template syntax. Templates are proper macros, abstractions over code in the sense of lisp. But C++ hides this fact by relying (mostly) on implicit template application and very, very, indirect syntax for template abstraction.
For instance: who came up with the idea of templated lambdas? Yes, manually managed closures are very C++, and yes, templated closures are also very much C++. But now we have templated closures that capture local variables IF THEY EVER HAPPEN TO BE INSTANTIATED. What the actual cluster eff?
Can you imagine C++ templates without template parameter deduction?
What's the alternative: spelling out all the parameter values, wherever the template is used?
mult<int, double>(x, add<double, double>(y, z));
C++ templates use type information to deduce parameters. Lisp macros may be expanded before any type analysis takes place.
The fact that you can just write:
mult(x, add(y, z))
and the language figures out the parameters for the mult and add templates from the declared types of x, y and z doesn't really have any parallel in mainstream Lisp dialects.
The idiomatic solution would involve making the functions themselves generic so they dispatch on the actual types of the arguments.
Anything relying on declared or inferred type would be a second-class citizen in a dynamic language anyway, where declarations are optional, and inference may be imperfect or absent.
It doesn't capture the variables "if they ever happen to be instantiated." They get captured once; the lambda gets instantiated. It's the operator() method that is templated, not the lambda object's type itself. Basically a template lambda might be equivalent to this:
As I point out now and then, there are two basic concepts here - closures and anonymous functions. A lambda is both. All four combinations (functions with/without closure, and with/without names) are possible. There's a tendency to confuse the two. An anonymous function is syntactical sugar. A closure is a run-time construct.
- Python and Javascript have nested functions with inheritance, which are closures. Most garbage collected languages have that, because it's easy to implement. Python also has lambdas, which von Rossum didn't want to put in but did to shut up the crowd that wanted them.
- Rust has nested functions without inheritance. A nested function can't access its outer scope. Rust also has lambdas, and they can inherit. C++ has now gone the same route. Non-GC languages need compile-time work to implement a closure, since something has to make a copy of what's inherited and worry about its lifetime.
- It's possible to have anonymous functions without inheritance. The classic example is a sort key function, where a sort function is called with a function that defines the test.