Hacker News new | past | comments | ask | show | jobs | submit login
An empirical study on the impact of C++ lambdas and programmer experience (acm.org)
108 points by uaaa on Oct 19, 2016 | hide | past | favorite | 105 comments

> After instructions, participants were given printouts of sample code they could refer to while solving tasks. Group Lambda got code of a C++ program using lambda expressions and group Iterator received code of the same program written using iterators. They then had time to study the samples before starting the tasks and could refer to these samples later.

These samples do not appear in the paper, so we don't know what they saw.

The “iterators” discussed are Java-/C#-style iterators, not C++ ones (as I expected reading the abstract).

In a C++ context I would have expected lambdas vs iterators to be something like:

    // lambda
    float retVal = 0;
    std::for_each(mb.cbegin(), mb.cend(), [&](item x) { retVal += item.price; });
    return retVal;

    // pure iterator
    float retVal = 0;
    for (auto it = mb.cbegin(); it != mb.cend(); ++it)
        retVal += it->price;

    return retVal;
... and the first would be better off as:

    return std::accumulate(mb.cbegin(), mb.cend(), 0f,
        [](float acc, item x) { return acc + x.price; });

I think the need to use ref-capture (since you only get a side-effecting `std::function` to play with in their sample) would be the thing most likely to throw people off – as it’s something that should be avoided in most code, anyway ;)

Yeah - from what I can see, neither interface looks like idiomatic C++.

EDIT: Looks like you beat me to the punch on some of these ;)

Instead of:

  float getSum(marketBasket mb) {
    float retVal = 0;
    // Implement solution here
    // ---------
    marketBasket::iterator iter = mb.begin();
    while (iter.hasNext()) {
      retVal += iter.get().price;
    // ---------
    return retVal;
I'd rather see real SC++L compatible iterators (as hopefully taught) and saner naming:

  float getSum(marketBasket market) {
    float sum = 0;
    // Implement solution here
    // ---------
    for (marketBasket::iterator item = market.begin(); item != market.end(); ++item) {
      sum += item->price;
    // ---------
    return sum;
And instead of being pre-provided with a function <void(item)>, if I'm reading the pdf correctly:

  float getSum(marketBasket mb) {
    float retVal = 0;
    // Implement solution here
    // ---------
    function <void(item)> func = [&](item theItem) {
      retVal += theItem.price;
    // ---------
    return retVal;
I'd rather see:

  float getSum(marketBasket market) {
    float sum = 0;
    // Implement solution here
    // ---------
    market.for_each([&](item theItem) {
      sum += theItem.price;
    // ---------
    return sum;
Or venturing into the far more functional style, where lambdas start to shine for me, personally:

  float getSum(std::vector<item> market) {
    // Implement solution here
    // ---------
    return std::accumulate(market.begin(), market.end(), 0.0f, [&](float sum, item theItem) {
      return sum + theItem.price;
    // ---------
It looks a bit better in C# where your selection of standard functions is a little less anemic and a little nicer to use:

  float GetSum(MarketBasket market) {
    // Implement solution here
    // ---------
    return market.Sum(item => item.Price);
    // ---------

"Idiomatic" doesn't necessarily mean better. I think objectively it's hard to argue that "item != market.end()" is superior to "iter.hasNext()". The latter accurately reflects the programmer's intent, while the former specifies an unnecessarily specific (and poor) implementation of the intent. First of all, using something like "market.cend() != const_iter" instead is arguably better practice (imagine you unintentionally omit the "!"). But programmers shouldn't need to consider whether the iterator is const or not when they just want to know if the loop is done. Also, consider the case where the vector is being modified (items inserted or deleted) inside the loop. It might be problematic either way, but "item != market.end()" is particularly bad in that situation.

Shameless plug: http://duneroadrunner.github.io/SaferCPlusPlus/#msevector

> I think objectively it's hard to argue that "item != market.end()" is superior to "iter.hasNext()"

Until you realize the purpose of the weird syntax in the former.

Namely: you can write algorithms where an iterator is a drop-in replacement for a simple loop through all items in an array via pointer arithmetic, if you write it to take a start and an end iterator as templates.

Plus said algorithm can take sub-ranges instead of requiring it to loop through all items.

I thought c++ iterators were very strange for many years until I understood this and other rationales.

Ah yes, that makes total sense. I suppose those that would like a nicer interface can just implement one on top. Like the proposed array_view<> and span<> interfaces.

> But programmers shouldn't need to consider whether the iterator is const or not when they just want to know if the loop is done.

and they don't: http://en.cppreference.com/w/cpp/container/vector/end - there's an overload returning a const_iterator. You don't need to use 'cend'.

And since insertion and deletion potentially invalidate iterators, 'hasNext()' is just as bad.

Yeah, maybe I didn't think the const thing through. I guess I was thinking that the benefit of using cend() is the double-check to make sure the iterator was declared as a const_iterator (when appropriate). But using "hasNext()", just like using "end()", you would lose the double-check.

Perhaps I could instead claim that "item != market.end()" redundantly specifies the vector, "market", and thus presents an unnecessary opportunity for mistakes? For example:

    std::vector<double> x_coords;
    std::vector<double> y_coords;
    for (auto y_iter = y_coords.begin(); y_iter != y_coords.end(); y_iter++) {
        for (auto x_iter = x_coords.begin(); x_iter != y_coords.end(); x_iter++) {
Notice the "x_iter != y_coords.end()" bug. I assume this will usually trip an assert if it is encountered in debug mode, but not in release mode. Of course you could just as easily mix up "x_iter.hasNext()" with "y_iter.hasNext()", but the removal of redundancy means one less opportunity for a potential mistake. Right? Hmm, I guess that's really an argument for losing the iterators altogether, which I guess was kind of the point of the study.

So then wrt iterator invalidation, I have a question. Consider this contrived scenario:

    for (auto y_iter = y_coords.begin(); y_iter != y_coords.end(); y_iter++) {
        if (5 == std::distance(y_coords.begin(), y_iter)) {
With conventional implementations of std::vector, the "y_coords.resize(3)" will presumably "invalidate" y_iter. And the "y_iter != y_coords.end()" will result in undefined behavior. But you could imagine "safer" implementations of vector<> that would instead throw an exception (or terminate or whatever). (Or you could actually download one of them at the link I gave.) So the question is, if this "safer" implementation supported "y_iter.hasNext()", would it be better for it to throw an exception (or whatever) in this case, or just return false?

> So the question is, if this "safer" implementation supported "y_iter.hasNext()", would it be better for it to throw an exception (or whatever) in this case, or just return false?

The safest option is to compile this code as Rust and have it fail to satisfy the borrow checker. And basic syntax parsing because this is C++ - but ehh, details.

Certain static analysis tools may also catch the issue. Well, if you're using real C++ iterators and not ::hasNext at least.

Sorry never mind, in that last scenario it's the "y_iter++" that will be the first invalid operation.

> "Idiomatic" doesn't necessarily mean better. I think objectively it's hard to argue that "item != market.end()" is superior to "iter.hasNext()". The latter accurately reflects the programmer's intent, while the former specifies an unnecessarily specific (and poor) implementation of the intent

At this point, I find the former much more readable than the latter.

I have seen the former a thousand times. I have seen the latter once - here, and here alone. Oh sure, I've seen similar patterns - under different names, usually under different languages - but I'm pretty sure this is exactly the first time I've ever seen it named "hasNext".

The worst bit is mixing SC++L terms - "::iterator" - and foreign idioms that do NOT conform to the SC++L concept of what I expect an "::iterator" to do. It is surprise and confusion of the worst kind. Even relatively inexperienced C++ programmers that I know would pause as I did, and ask - "Wait, what the fuck?", and then waste the next few minutes rediscovering what the hell the code is doing (absolutely nothing special that would call for special divergence from the norm.)

As such I must thoroughly disagree on the strength of the communication of intent between the two samples. The former might as well be plain English to me - the latter might as well be pig latin. Oh sure, I can figure it out - but it'll take me a minute. It DID take me a minute.

You want to steal patterns from Java or C#? Then at least name them after the Java or C# concept. Call it enumerator. Embrace the fact that you're using Java or C#'s idioms. Decry C++'s idioms as poor choices by the C++ language if you must. And hell, yes, there are exceptional cases where you can say "let's go with none of the above - the best solution is unconventional and unusual, using the normal idioms of no language."

You have very much failed to convince me that this is one of them.

And while you could perhaps argue that C#'s idioms are superior, I would note that you're a few decades late to the party on that count, and talking to the wrong guy. I wouldn't even disagree, necessarily. But C++'s idioms have a useful inertia at this point, and are frankly not that bad. Rather reasonable, even. Although they do have a learning curve.

> First of all, using something like "market.cend() != const_iter" instead is arguably better practice (imagine you unintentionally omit the "!").

I prefer to have a warning-as-error for the unparenthesized assignment you posit (if only because my coworkers probably didn't use yoda conditionals on the existing codebase). But hell, even yoda conditionals would be better.

> But programmers shouldn't need to consider whether the iterator is const or not when they just want to know if the loop is done.

Realistically, I won't - instead preferring auto, or never naming my iterators at all, as I've done in the std::accumulate example - as begin is overloaded appropriately.

> Also, consider the case where the vector is being modified (items inserted or deleted) inside the loop. It might be problematic either way, but "item != market.end()" is particularly bad in that situation.

Oh, I know what I want in this case! Iterator debugging to catch the problem. The PDF has the source code to ::hasNext:

  bool marketBasket::iterator::hasNext() { return owner−>items.size() − index > 0; }
Do you see iterator debugging? I don't. What happens when index is greater than .size() because of a removed element?

Based on other code, we know items is a std::vector. And thus that .size() is unsigned. And thus that the result of ...size() - index is unsigned. And thus that the result will underflow to a huge number, and that ::hasNext() will return true, leading to a buffer overflow.

If "item != market.end()" is particularly bad, then "iter.hasNext()" must be particularly downright evil, for it's doing even worse.

Note that any fix applied to ::hasNext could equally be applied to comparing against .end().

> You have very much failed to convince me that this is one of them.

Yeah, after further consideration I'm not convinced either.

First two tests used lambda with custom for each method. If authors used real c++ iterators and for each loop it would become obvious that comparing iterators and lambdas at iterating is the same as comparing lambdas with if statements at being if statements.

I agree that the "iterators" in the paper are not C++ iterators as found in the STL or language spec.

Context always matters. I use lambdas sparingly in my applications, except for one major area: user interfaces.

I can't begin to stress what a huge timesaver it is being able to bind a button's callback to a quick lambda instead of having to bind a callback to an std::function, add the function to the class header, and then put the actual button-click code somewhere else in the project in a separate function ... and then repeat that process for a large UI with hundreds of widgets.

It's not even the initial extra typing, it's having all the code right where it's most relevant instead of scattered around everywhere and having to name all of these callback functions.

Yeah I think this is the real win - the examples in the paper don't really cover any of the real reasons why someone would use functional programming. The places that I've used C++ lambdas a lot are callback-heavy code, i.e. things with a lot of asynchronous I/O, multi-threading, etc.

While I don't doubt the validity of the argument that it takes longer for a programmer to write correct lambda code in C++ (I have been using them since C++11 was released and still forget the capture list syntax sometime), it's not much more than an academic exercise to take some iterator-based code and replace it with higher-order function calls. It's unfortunate that all the tasks seem to be based around doing that though. I do think it is still reasonable to do this in C++11 once you know how - with -O1 turned on you can get basically the same code spit out using std::transform, std::accumulate, std::for_each, et al, as you would using a for loop.

It's also unfortunate to note that, at least in g++ and clang, there is still significant advantage to using lambdas over std::function and std::bind. Lambdas typically end up being faster in both compile-time and run-time, and end up generating less code. For these reasons I've found myself using them a lot more, especially anywhere I would have done some type of currying. This I do miss somewhat, but it's still leaps and bounds ahead of something I would have written in C++03.

> It's also unfortunate to note that, at least in g++ and clang, there is still significant advantage to using lambdas over std::function and std::bind.

I have to say, std::bind is just a travesty. It's so difficult to bind a member function pointer that takes multiple arguments to an std::function.

I wrote my own so that you can do this with just: function<void (int, int)> f = {&Class::func, &object};

Source is here: http://hastebin.com/raw/kobudabasa

In doing so, it becomes clear there's basically two ways to implement this idea:

1. you allocate heap space to perform type erasure. This results in a pointer indirection plus a virtual function call worth of added overhead. Along with tremendous costs to allocate and destroy the function objects.

2. you store a big chunk of raw memory inside the function<> class, and cast it as necessary to a pointer. This is actually what I did prior to C++11 and lambdas. It was tricky because the size of member function pointers is undefined. Having a vtable makes them larger. So for that I made a complex dummy class to pull its sizeof info.

Option 2 is definitely a good bit faster (at least for constructing/copying/reassigning/destructing them), but you're really butting up against undefined behavior and abusing the language. And capturing lambdas would be even more challenging.

But even with that, yeah, you can't ever really beat concepts that are native to the language like lambdas and virtual functions themselves. Compilers can get really clever and inline things in a way that's exceedingly unlikely to ever occur with std::function, no matter how you implement it.

I've also written my own "version" of function but intentionally lightweight, for an embedded project: https://github.com/ambrop72/aprinter/blob/master/aprinter/ba...

Callback<void(int, int)> f = APRINTER_CB_OBJFUNC(&ThisClass::function, this);

One magic thing is that the macro is just a value, it figures out the type itself.

There's zero memory allocation. The Callback class just contains a function pointer and a void pointer. So you can't bind argument values other than the object pointer, but I have no need for that.

This is ridiculous. C++ lambdas (and std::function) don't replace iterators except for the most fervent disciples of the Church of Haskell. They replace single-function interfaces in cases where you would have had to put together a custom struct that did exactly the same thing as a lambda with capture but in about 15 more lines.

To be fair, the most fervent disciples of the Church of Haskell would use Haskell I'd think :)

I only scanned the paper, but I get the impression the title was chosen specifically to grab attention. "An empirical study comparing the use of C++ lambdas versus C++ STL iterators and programmer experience" wouldn't get eyes on their presentation, and eventually, being posted to HN...well, it would, but they'll surely get more reads/downloads this way.

Clickbaiting for conference proceedings, who would have known.

Such a title wouldn't be right, either. Their poor iterators don't seem to act like real iterators.

Don't you mean the church of A. Church? ;)

Yes, especially irritating in C++2003 here at work! you realise how useful the capture list is with lambdas when you have an in-function struct that can't see outside its own scope so you need to pass everything in (and won't have access to private portions of the outer class).

Lambdas really are great.

> and won't have access to private portions of the outer class

Nested and member function local classes are actually implicitly friends of the containing class. Not all compilers were conforming in this area though.

I'm not so sure. Generally implementing an iterator when the iteration doesn't represent a straightforward walk over the elements of a collection is much more error-prone. The implementation of the lambda version looks exactly the same as if you were just performing the iteration locally, while the iterator version requires saving and restoring a bunch of state and handling more edge cases. At call sites, the iterator version looks a little better, but not so much so that I'd consider the additional implementation complexity (and hence potential for bugs) worth it. That said, this is definitely a controversial opinion and there are arguments in the other direction as well.

Writing iterators is non trivial, but, lacking first class continuations, external iterators (a-la begin, end) are more powerful than internal iterators (a-la for_each), as they allow iterating multiple ranges at the same time and easily defining subranges.

For a long time I could not understand why anonymous closures were not implemented in c/c++, and even had arguments on forums, but then realized that people just do not think in that terms. You can provide lambdas, but they will use these for iterators or similar bs. And there is no better technique or framework for the case you described, they just don't do that at all.

The paper seems to mainly compare iterators vs lambdas. This seems like a bit of a strawman; the best use of lambdas is beyond iterators.

For example, consider callback heavy asynchronous code. A promise library with lambdas is much easier to write and read than the equivalent state machine.

I would go as far to say any mechanism where function chaining is useful, such as the nice data to mark/SVG abstraction used in D3, and also in promise libraries, has advantages with lambdas. Not only do you avoid having to write extra classes or methods, but the code is more succinctly logically grouped together, requiring fewer indirections to get to the transformations occurring.

Basically anywhere where you would have used a callback in C code could probably benefit from a C++ lambda. It's easier to see what's going on, you don't litter your code with hundreds (or thousands) of tiny functions, and the compiler can easily inline everything. The fact that you can capture whatever you need makes it super easy to use inside a class if needed (eg; you need to access class members).

It seems really dumb to me to declare that lambdas are detrimental to novices when it's clear they have a great deal of utility outside of something like replacing iterators.

I'd much rather have a lot of smaller functions with single responsibilities, but then being middle management I worry about things I didn't when developing.

I need the code to be SOLID, I need the time to market to be as small as possible and I need to be able to replace any developer with any developer on a moments notice.

When students don't know lambdas you're costing me money by using them, because you made the training process longer.

If you want solid code in C++ you (essentially) have to only hire people who are good at their job, or accept a long runway where lambdas would simply be taught instead of the inferior methods/patterns they are intended to replace.

I have yet to see a C++ codebase which was good while not written by people who are essentially C++ experts, or through in-depth code reviews by such, for all code. I understand finding the talent may be hard, but then the C++ volume might need to be decreased and replaced by something less demanding, or the code will likely be anything but solid.

This is a rather shallow perspective on software engineering. You are basically saying you don't want your employees to use the language as it is designed, even when it provides tools to make code better.

I'm glad I don't work for you...

I think I understand why turnover is such an issue. The reluctance to invest in your people is a little baffling.

> I need to be able to replace any developer with any developer on a moments notice.

As unpleasant as it is, this is all too widespread, because it seems like common-sense on the surface. "A little knowledge (on the part of management) is a dangerous thing."

This is amusing and I think (hope?) it's sarcasm.

Interestingly, you can think of lambdas a small functions with single responsibility, which happen to be easy to find (they're inline right there in the code and you don't have to hunt around for them).

>A promise library with lambdas is much easier to write and read than the equivalent state machine.

async/await makes this even easier, AFAIK C++ is getting it soon as well.

I'm a professional and you can take the lambdas out of my cold, dead hands.

Also, their examples are removed from reality. I've never seen people use lambdas like that. Most of the use cases I've seen are callbacks that get triggered on certain events (i.e. "display notification when background loading thread completes") and predicates (i.e. find_if). I see neither in their examples.

Of course students had problems—-it's one more damn thing for them to learn about C++, likely from professors that don't know it very well either. But <algorithm> is way more useful these days, and boost::asio is a dream.

Unfortunately most teachers aren't like Kate Gregory, and I fully agree with her, they should stop teaching C -> C++.

Already in 1994 it was obvious to me that it leads to bad unsafe code and worse, the mentality to micro-benchmark each code line.

There's pitfalls to everything. I find that a lot of people that only learned C++ end up writing nice-looking safe code that's very well organized but has awful memory usage and dubious performance characteristics. It's not that they're bad programmers or made poor algorithmic choices. It's that they've been indoctrinated with the fear of "premature optimization". Moreover, they don't really know any better if they do have optimization opportunities.

On the flip-side, the C programmer who starts out learning C++ will often spit out some hideous abomination that uses no namespaces, consists of obscure function names, pointers everywhere, and tons of callbacks.

Thing is, sometimes performance really matters. A large part of the HN audience focuses only on web-oriented programming. But in scientific computing, finance, etc. being able to squeeze out a few more operations/CPU cycle can be incredibly important.

I think rather than being dogmatic about the issue, as programmers tend to do, it's important to introduce students to C but be very clear about why you might want to use some of its features vs. relying on the safer C++ variants.

Agree, but most of the time performance matters less than people think.

In all my years as developer, even back on Spectrum, MS-DOS and Amiga, I never bothered turning off bounds checking and it seldom mattered for the type of applications I was writing.

The very few times it mattered, I was writing big chunks of Assembly anyway.

Everyone thinks their applications have the same performance requirements as Microsoft, Apple, Google, Facebook, CERN, Wall Street, Sony ... have, but they don't.

It is like the native code version of going web scale on day 1, when everything that one has are a few pages to display.

Fear of premature optimization, fear of pointers, and fear of how computers work in general, yes.

Pointers should be used very sparingly, and much of the time in code I've seen they could have and should have been avoided. But it seems like we've come to some far extreme other side of that thought to the point where seeing a pointer instills an exaggerated fear of memory stomping, leaks, and a variety of other things. These are important concerns, but, like optimization, ought not make a person afraid of their use. Anybody who needs to do non-trivial programming at the systems level had better get over those fears and focus on learning how best to use the tools they need to, in my view.

Out of interest, I never learned C but went straight to C++. What in particular about C++ makes you believe that you'll write memory hogging programs?

Specifically, the STL has a lot of slow containers that people readily use. For example, a beginner might see a std::map<T> and think "wow, a handy hash table class!". First off, it's not a hash table (usually a RB tree). Second, the closest thing to a hash table in the STL (std::unordered_map<T>) is still pretty slow.

Then there's std::string. Super nice in 95% of cases but can cripple an application if you have millions of strings you need to deal with (TONS of dynamic memory allocation).

And then there's std::shared_ptr. Super convenient but potentially a huge impact if you have items with very short durations in hot loops. std::unique_ptr on the other hand has no added overhead. Sure, you can look these things up. But it's not really obvious.

C++ is an incredibly useful language. It's far from the safest, prettiest, etc. But when used properly it's a very powerful tool. But knowing C can help you to better understand when you're not getting something for free and when undefined behavior can rear its ugly head.

I see, interesting, thanks. I suppose the same problems arise attempting to use STL algorithms instead of using algorithms that are better suited to a specific container, eg. std::set::find instead of std::algorithm::find.

Being aware of what is happening and the best tools for the job I suppose! And knowing the STL inside out and its idiosyncrasies and foibles.

I've seen a game engine built on shared_ptr that had trouble getting a 2D game performing well on the PS3.

Its not that C++ will bloat your program, its that many programmers don't understand the tradeoffs of the primitives they're using, or don't have the experience or vision to see what they become at scale.

That last bit of your sentence is a great point - no vision to see what they become at scale, ie normally monstrous

I see that mentality in every language, even if its worse in C++ because "close to metal" and "we know C++, therefore we must be good programmers".

What usually happens is that the code gets unreadable/unmaintainable quickly and is still a complete unoptimized mess at the macro level.

Once you realize this you don't need C++ for most application domains. And for the domains where C++ matters you also realize you'd be better off in C with a scripting language on top.

Actually that is how I have been using C++ since around 2006, as an infrastructure language when JVM, .NET or ML languages cannot fulfil my use case, which happens very seldom.

And even though it was one of the languages I enjoyed most using after Turbo Pascal and I even gave C++ classes at the university, I am a firm believer that if Java, VB.NET and C# had been fully AOT compiled from day 1, just like many other alternatives that used to exist, C++ might have not taken off as it did.

The rise of VM languages, with other AOT compiled languages fading away and rise of FOSS written mostly in C, made C and C++ the only languages we could turn to when the performance was lacking, thus arriving at the actual situation.

As for using C with a scripting language, without the safety of strong type checking, real enumerations, support for arrays and strings, no namespaces, no RAII, new/delete instead of malloc()/free() and many of safety improvements from C++ over C, that is not something I will ever advise.

I already did that once with Tcl for two years, no need to repeat it.

In my university we started with C++ and pretty much everything was done in it (4 years). Although I've had C experience before that, I'd say that teaching C -> C++ to a complete beginner will be pretty hard on them and won't recommend it.

Title of the research paper should be "C++ is not Haskell", but snark aside, I find C++11 lambdas useful to replace old-school callback code (of course only if callbacks make sense in a situation) because:

- if local variables need to be captured, the code is much less noisy than using the old std::bind mess

- non-capturing lambdas also work for C-style function pointers, and in this case the generated code is very efficient (no std::function object created under the hood)

If you're aware what happens under the hood (...that in most cases a std::function object will be created, which in turn might do a dynamic memory allocation to hold captured variables), lambdas are a useful syntactic sugar and one of the more useful additions in C++11 (IMHO).

I'm pretty sure that most uses of lambda don't involve creation of std::function (and thus dynamic allocation and virtual function calls).

As long as you maintain the distinct type of the lambda, any captures (by reference or by value) will just be fields in a stack object.

The only true conclusion of this study is that students find C++ a bit intimidating.

Which is not surprising at all, because C++ is a very complicated language !

It takes years, even decades before you can master this language - even then you probably won't be using all of its features.

Back in the day, it took me a long time before I could truly grasp the C strings and pointers in general.

Yes, I could write code with pointers and C strings (new char[length + 1] and delete[] str; were my buddies!), but sometimes I was programming with my eyes closed and fingers crossed, because the implications of sharing raw pointers were too complex, especially if threads were involved.

Same with these new features. They may 'look' more modern due to the updated syntax, but the underlying concepts that they are hiding still need to be well understood.

So not surprising that students struggle with getting it right the first time. The good news is that with a bit of experience this becomes easier, as with anything else..persistence is key.

Yes C++ is complicated and yes students struggle with programming. It takes years to understand how to put a decent program together (separation of data, logic and transport etc).

As for your guesswork with C-style strings, you should have been using std::string and using copy constructors to pass work into a thread (eg. who owns the data the thread is working on?). With modern things you can move the data into the thread instead of copying it.

I would recommend students working on their own personal projects as a good pasttime as it further helps anyone understand how to write a decent program and learn the language. Also, tell them to read Stroustrup's book.

I started programming back in the day when every programmer wrote his own String class :).

Standard library was, for one reason or another, unpopular back then. Or maybe because of the C Windows API and later MFC, which came with its own CString class...

Add to the confusion CString, BSTR and std::string and std::wstring. What could go wrong?

Some people feel comfortable expressing things in a more traditional way, in part, because you cannot change the habits built over the course of decades by just announcing a new standard.

After the standard including lambdas came out, compilers did not immediately comply to it, and it took some time for them to catch up. Then it took even more time for tutorials and books to catch up. And it will take time for the C++ community as a whole to catch up as well.

Compilers (at least clang, MSVC and GCC) released versions supporting lambdas well before the C++11 standard was finalized.

I see. The feature parity wasn't 100% I remember. There was also Boost Lambda since at least 2004

Lambdas are an incredibly useful construct, and, once you master them, they do make a lot of programming tasks much easier.

However, C++ lambdas picked the most horrifically ugly syntax possible, and they switch between three subtly different semantics (copy, reference, move) depending on a single glyph. I feel bad for the people working on modern C++ - maintaining backwards compatibility is a huge constraint upon design space.

I'm not surprised that lambdas slow down people that aren't already experienced with them. Since I rarely program in C++, whenever I go back to it, I always have to spend a bit of time bashing my head against the horrific syntax.

C++14 improves upon lambda captures a lot. I'm also not aware of a move capture, only "by reference" (&) and "copy" (=). In C++14 you're allowed to put arbitrary assignments into the capture, e.g.

    [foo=std::move(bar)]() {}

I did that in the C++11 days by creating a bastard class template that would subvert the copy operator and turn it into a move and capture that by value.

It was ugly as heck and was removed as soon as I have c++14

Serious question: how would you improve the syntax while maintaining their feature set (which is not huge, but includes a few important things)?

I understand that C++ lambda syntax can be confusing on first encounter, but after reading about them for five minutes it should be very naturally comprehensible to any experienced C++ programmer. It's terse, but very clear.

As a working C++ programmer for over a decade this conclusion is absolute insanity to me. People (myself included) are actually using the STL algorithms now because you no longer have to go through the pain of writing a functor. Every time I can avoid writing a for loop I'm a little happier (and my code is a lot more readable).

When lambdas first came about there was a lot of "meh, I could already write a functor just fine" but the ease of using them is just so much nicer and the readability of the code is so much greater. Just writing a plain for loop would often be less code than using an STL algorithm and a custom functor so there was very little incentive to use the highly tuned STL algorithms if it was just going to take more boilerplate. Now there is and we are finally seeing C++ programmers catching up to the insights that functional programmers had made a long time ago[1]

Others have said this but comparing lambdas to iterators is a very odd decision because they are completely unrelated things.

The conclusion should really be that anonymous functions aren't as intuitive as "regular" imperative code. The regular code walks you through what is going on in babysteps so even a new user can figure out what is going on. The way with lambdas requires a bit more knowledge but for working programmers who aren't learning C++ for the first time they are obviously a big boon. Sometimes you have to learn something slightly more complex to reap the benefits of it. Pointers are very hard for brand new programmers to wrap their heads around but nobody would argue that judicious use of them can be a real benefit (or essential, even) in some situations. This is like concluding that smart pointers are bad because they are less explicit about what is going on. You still need to learn C++ manual memory management but your code will clearly benefit (both in terms of verbosity and memory safety) by using smart pointers 95% of the time.

1. CppCon 2016: Ben Deane "std::accumulate: Exploring an Algorithmic Empire" https://www.youtube.com/watch?v=B6twozNPUoA (std::accumulate, fold essentially, has been in C++'s STL since the beginning and Stepanov probably had it in mind before C++ even existed but we are just now getting lectures like this and I think lambdas are to thank for this)

It's important to note that the negative impact documented in this study is in development time and whether the task is completed. It makes no claims about impacts on maintainability that show up later in the development process. It also does not measure the longer term impact on development time if a team starts using lambdas and gains experience over time.

I say this not to dismiss the study, which appears to be fairly well done and to provide interesting results. I'm simply saying that its results are not inconsistent with lambdas providing a net, long-term benefit if introduced to a development team at work.

Submitter's editorialized title mischaracterizes the OP

> All the tasks focused on iteration on a collection using a C++ vector object.

That's comparing "functional style" collections to iteration.

It is readily apparent that C++ syntax is so troublesome that "functional collections" won't be a win for small iteration blocks.

We had the same debate for functional Java collection methods (although the Java 8 lambdas tilt it more closely in functional Java's favor)

The main use case for C++ lambdas is for declaring callbacks.

As someone who writes a lot of asynchronous code in C++, lambdas have changed my life.

The paper seems to study the use of lambdas specifically as an alternative to iterators. I actually mostly still use iterators myself, rather than lambda-based alternatives, so I agree: they don't have a huge impact there.

But the headline is misleading. It should have had "when compared with iterators" added to the end.

Recently I wrote a parser generator, which would take a rule structure and return a function that does the parsing. Lambdas were very useful here, and I do not know how I would've implemented this without them, at least in an efficient manner, because they let me do something like this:

    Parser ParserGenerator::compileLiteral(Rule& rule){

        const string literal = rule.value;

        Parser parser = [literal] (shared_ptr<State> state){
           //parse the literal...
           return state;

       return parser
Solving this without lambdas would require a different encapsulation mechanism for the rule data, such as a class, which (IMHO) would make the code less idiomatic.

So lambdas are actually very useful!

Well, an alternative approach might to not to couple parsing and state manipulation :)

PEGTL does this. Approximately, you have a template parameter MyAction<> on the Parser<> template which in turn calls "MyAction<Rule>::apply (state)" when parsing of each rule is complete.

It's a fantastic library. I highly recommend giving it a whirl.

[0] https://github.com/ColinH/PEGTL/blob/master/doc/Actions-and-...

Can you always replace lambdas with iterators? For example, to visit a graph you could implement it using function that takes two arguments; the graph to visit and a lambda holding the function (functor) to apply to each node.

It is fairly easy to implement and the compiler will inline the functor so there is no performance penalty. Any solution based on iterators that I can think of would be much more complicated and would probably not lead to as efficient code.

I'll admit that the "user interface" for iterators are much nicer than using functors. The former meshes well with c++:s for loop syntax while the syntax for lambda constructs is bad.

I used to really like C++ ... back in 2000. That is before they added the kitchen sink into the language.

I prefer my computer languages to be like Chess - a few rules but efficient and expressive. Like C.

Why? Because everyone can read the code others on the team wrote, without being a language lawyer and knowing tons of esoteric features and magic.

That has implications for maintainability and team productivity, and the bottom line for a business.

Really, people, having basic coding style conventions instead of tons of language features wasn't so bad:

  int Foo_bar(struct Foo* this) {
     // even this is readable

Very few people can read and understand C without being a language lawyer an knowing tons of esoteric magic. There are many things in C which look perfectly reasonable but which actually result in undefined behavior, and you cannot reason about what a program will do in the presence of undefined behavior.

Further, C makes it impractically difficult to handle things that should be simple. Like, uh, errors and resource cleanup. The inability to have something as trivial as a scope guard that works as a human being would expect it to work will ensure that under no circumstances will I ever write a piece of C that anything I care about relies upon. I know I'm not good enough to write perfect C, and imperfect C is disastrous. (The fraction that is good enough to write perfect C is a vanishingly small proportion of those who think they do. And try to. And fail. And hurt other people in the process.)

But there is a difference between using C in crazy ways, and using 20 features of a language in ways that make them interact.

The more features a language has, the more I have to know just to read someone's code or be productive in a company - and the more chance someone doesn't know about potential harmful interactions and side effects.

> there is a difference between using C in crazy ways

As per my sibling, write me something as straightforward as a C++ RAII-using dtor in C without "using C in crazy ways". I will not hold my breath. The cost of a very minimal subset of C++ is literally zero, and yet significantly improves the likelihood of your code actually working. You're picking social baggage in either case: either understanding the subset of C++ that your team is using or expecting all of your developers to be perfect where C++ (and other languages--shouts, Rust!) just do it for you, correctly.

Technology is socially interesting in that incompleteness and inexpressivity is so often misread as "elegance" or "minimalism". That a language is "simple" is not a feature when it offloads all of the danger onto the (almost invariably failing to consider critical information at the worst possible time, and I include myself at the forefront of that characterization!) developer. This is why we have tools that compensate for the most common, and most destructive, of our mistakes.

(This post should not be construed as any particular endorsement of C++. C++ is a gong show when used improperly. But at least it's possible for a mere mortal to use it properly.)

The reason I dislike C++ (since about the 2000 as well) is that while it is possible to shackle yourself to only using C++ RAII etc, the vast majority of code in the wild (and in libraries, etc) does not. It does 90%, but that doesn't get you 90% of the benefit (maybe 50%, maybe 0%, depending on your point of view).

You can do essentially all the "C in crazy ways" in C++ as well, and people do. In my opinion and experience, it isn't what the language provides, it is how it is used in practice - and again from my experience (YMMV), C is used sanely and C++ is not.

I think the profusion of use-after-free bugs and memory leaks in C code running all over the place should put the lie to this. And, further, modern C++ allows you to firewall off the damage of bad C++ and most C, when you are forced to interact with it, via unique_ptr and your own enforced RAII. (And the idea that using this is so pejoratively "shackling" is bonkers to me; being "shackled" to the use of railings on walkways over a pit of fulminating acid is just the worst.)

The idea that people use C "sanely" more often than C++ (or, you know, something actually good--further shouts, Rust!) doesn't pass the smell test. Are you checking the retval of every sprintf? Are you writing goto drops in every method where there's allocated memory, diligently checking every error code, and properly bailing out, every time? If so, you're that one percent. But you're probably not. And that's not a slight--I'm not, either. That's why I am proud to count myself as a member of a tool-using species, because we build (and have built) better tools to compensate for our flaws.

"shackle" was a bad choice of word, I agree. I meant to say that you might be disciplined enough to never use anything except RAII, but most (users/libraries) aren't.

> If so, you're that one percent.

I probably am that one percent (and I don't use sprintf, because there is no sane way to use it). When I say people use C "sanely", I do not mean that they never err in any way, far from it. And my observation (your mileage obviously varies) is only that C code bases that I tend to use and meet (e.g. x264, ffmpeg/libav, the linux kernel, the Nim compiler) tend to be saner than C++ code bases that I tend to use and meet (e.g. libzmq, although that one improved dramatically since 4.0, and is now almost sane, boost, stl)

I admit that I have not yet met a C++11 codebase with lambdas - that might have restored sanity. But even if it does, it does not retroactively bestow that goodness on the millions of lines of code already out there.

I stress again - I am not passing judgement on the language, but about how it is used in practice, through my own sample set. If I work on a project in which I can dictate the exact subset, choose the people, etc, I might pick C++. But in most projects I'm involved in, the constraints are dictated in some way or the other that makes C at least as good a choice (and often better) than C++

I completely agree and would, most of the time, choose C over C++!

Except, that I have seen C++11 codebases that heavily relied on lambdas. And that was far from sane. I am very familiar with lambdas from pure functional languages and partially ones (python, ...). But all the syntax specifics, brackets, ... in C++ made it a very annoying process to understand, what was even going on at all.

Using short functions would be more lines of code. But at least I would have known right away what's happening in the code.

And we already could take advantage of many of those positive features with C++ARM.

Hence why I eventually did a Turbo Pascal -> C++ transition, with a very very short stop in C.

I was lucky that our technical school also had access to Turbo C++ 1.0 for MS-DOS on their software library. As I was not getting why should I downgrade myself from Turbo Pascal 6.0 into C.

Already in those days of "The C++ Report" and when "The C/C++ Users Journal" was still called "The C Users Journal", there were articles how to put C++ features to good use for increased safety.

And this is a major culture gap between C and C++, that I have observed since then, yes we also get the performance obsessed ones, but there are also the security minded ones.

I seldom see talks about how to increase type safety in C, their C99 security annex is a joke as it keeps pointers and size separated, and is so highly regarded that it was moved into optional in C11.

C++ community on the other hard improves the standard library to decrease the unsafe use cases, promotes guidelines and is trying to reduce the amount of UB use cases.

This is a really interesting notion. I'd love to follow up with you offline. Drop me an email?

So that's easy:

  #define NEW(t, args...) ((t*)malloc(sizeof(t)) && t ## _construct(args))

  #define DELETE(t) (t ## _destruct() && !free(t) && (t = NULL))

This isn't sufficient unless you consciously error-check and goto-trap every single failure point in your code. Which you might do. But you'd be the literal one percent. If not the literal one permille.

In DELETE(), is t the variable name or the type name? It is used as both, which won't work.

Yes, RAII and exceptions (as well as a few other things like being able to declare variables anywhere) is the reason why I liked C++ in 2000 more than C.

But beyond that, it all went downhill. There was no reason to make C++ the most bloated language ever.

The same is true of Javascript etc. Newbies will not be able to start, now, because they will think about let/var/const the same way as in C++ they have to think about all the possible casts, copy constructor vs casting semantics etc.

Tell me this for example, does the following use a cast or a copy constructor?


"C++ has optional and complicated things that incur costs only when you use them, so let's not even have dtors" is...not a good look, I think.

Gotta love those non virtual destructors which only clean up the base class. And the ambiguity about whether you're going to call a type conversion operator or a copy constructor when assigning A a = b.

Because you know, it's all super clear.

Who said anything about inheritance? Like, at all? That is yet another feature you pay for only when you adopt it. You can pick one of two problems: you can agree upon a subset of C++ to use or you can expect your C developers to be perfect all of the time (while having fewer, if any, ways to express correctness). Dragging out additional things that are not part of your subset of C++ doesn't help formulate an argument against this.

I agree about C needing memory management and exceptions. That's why I said I liked C++ when that's all there was. Classes were nice sumyntactic sugar.

But the language jumped the shark starting with C++0x

C++0x adds nothing you are obligated to use, though. No sharks have been jumped unless you choose to strap on some skis and rent a boat. (Meanwhile, with move semantics and unique_ptrs, C++14 is actually way nicer as far as memory management goes than that 2000-era C++ was.)

Being a language geek with major in compiler design and systems programming, I tend to be a bit language lawyer.

I seldom meet C developers that are able to distinguish between ANSI C and my compiler's C, that extrapolate from my compiler's C version Y, how the language should behave.

Then they port the code to my compiler's C version Y + 1, or another compiler vendor, their code gets broken, blame the compiler, only to find out that the code was already broken from ANSI C point of view.

So is your compiler ANSI C or not? c89 or c99?

I am writing about C developers' knowledge about the language.

> But there is a difference between using C in crazy ways

Is signed arithmetic using C in crazy ways? Is shifting by an arbitrary value without checking to make sure that the number of bits is in the valid range of the type using C in crazy ways?

Undefined behavior is everywhere in C.

And all of those are inherited by C++. Thankfully C++ has a large assortment of additional footguns to help combat the previous footguns. It also has armored, self-detonating shoes.

In my experience, some people are too much attached to that good feeling of knowing "all there is to know" about something; this has the taste of proficiency, but the very real risk is to prefer the small pond because of that, calling the sea "overextended".

Admittedly there is a lot to learn to truly master C++. But I find RAII (and particularly move semantics) far more usable and manageable than the C-style C++ code I have seen.

As an example, you do not have to be a language lawyer to understand that C-style casts are horrible and dangerous, and really will lead to colossal implications for maintainability, team productivity and the bottom line for a business (it is a static_cast or reinterpret_cast? Who knows?!)

If you're employing people who only know half of the language or never finished chapter 1 of Stroustrup's C++ book, then I would say it is a problem with your employees than the language.

There's a guy at work whose position is unclear to most. It's just that random guy.

Typically, on monday mornings and friday evenings, he's very loud and looking busy. This is an age-old trick to superficially keep your job.

Ever so often one would hear his odd sounding voice pierce through the office talking about some idiosyncratic evidence while looking proud of himself.

Truly, if this guy was a programmer (which I doubt), he would be using lambdas. See, lambdas might have a use after all...

I have used lambdas heavily in the past and then stopped. Getting away from the rabbit hole that are lambdas + SFINAE + variadics increases productivity immensely.

It seems that the most important conclusion of the article is that using lambdas is more error prone than using iterators. What begs the question: how many compiler errors were caused by the actual lambadas, and how many were caused by the declaration of a `std::function` variable, as induced by the authors (that besides unnecessary, is somewhat of a bad style)?

Paper says that "those in lambda group received no benefits in regard to [...] compiler errors".

Really, would anyone expect lambdas to improve compiler errors? Also, isn't that a problem of the compiler, and not of the lambda idiom in C++ or C++ language itself?

I mean, idk about clang, but at least compiler errors from msvc and gcc have always been hard to understand, with or without lambdas.

Is the paper available in public domain? If not, I won't regret much. Looks like it's badly written and filled with un-idiomatic C++ code.

How and who allowed them to publish such a ridiculous paper !!?? There is so many un-idiomatic C++ code... And does the topic even fit to be published as part of ACM ??

I'm so relieved the consensus is negative here.

Applications are open for YC Winter 2022

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact