Hacker News new | past | comments | ask | show | jobs | submit login

Taking a concept such as a lambda function and making it look this ugly...this is why I hate C++. I wish I wasn't forced to program it every day.



How is it ugly? The capture list is a necessary complexity in a language with manual memory management.


It's a trade off more than a necessity. For example, Rust doesn't have explicit capture lists, and if you want explicit control, you make new bindings and capture those. You almost never need to do this in Rust, so it's optimized for that case; I haven't written many closures in C++, so I can't say as much about the frequency there.

To make this more concrete:

    let s = String::from("s");
    
    let closure = || {
        println!("s is: {}", s);
    };
    
    closure();
If you wanted to capture s in a different way:

    let s = String::from("s");
    
    let s1 = &s;
    let closure = || {
        println!("s1 is: {}", s1);
    };
    
    closure();
No capture list needed, same control.


Rust doesn't have explicit capture lists, but it does have the `move` modifier on closures which is like C++'s `[=]`.

Strictly speaking, Rust probably could have gotten away with having neither the `move` modifier nor capture clauses at all, but it would have had wide-ranging implications on the ergonomics and capabilty of closures.


How would you do the equivalent of this in Rust?

  auto on_heap = std::make_unique<MyType>(...);

  function_that_accepts_lambda([obj = std::move(on_heap)]() {
     obj->bar(...);
  })


    let on_heap = Box::new(...);

    function_that_accepts_lambda(move || {
        on_heap.bar();
    });
This is sort of what kibwen was mentioning: move is a single annotation that overrides everything to capture by value rather than have it inferred.


What if you want to move some things, but copy others?

e.g.

  auto shared = std::make_shared<MySharedType>(...);
  auto unique = std::make_unique<MyOwnedType>(...);

  function_that_accepts_lambda([shared, u = std::move(unique)] {
      shared->foo(...); u->bar(...);
  });

  // Outer scope can still use shared.
  shared->foo(....);


I am 99% sure this is identical:

    let on_heap = Box::new(...);
    let shared = Arc::new(...);

    let s = shared.clone();
    function_that_accepts_lambda(move || {
        on_heap.bar();
        s.foo();
    });
We have to make the extra s binding.

Also, my sibling is correct that Copy types will just be copied, not moved.


If the type implements Copy they'll be implicitly copied when moved into the Rust closure(I think). Or you can declare a scope var and clone() manually.


The syntax is not the prettiest, but it is legible once you understand what [](){} means.

In C#, there is no such thing, but there is a part of me that wishes we had such a thing. I like the ability explicitly state what variables are being captured.


> I like the ability explicitly state what variables are being captured.

Why? You state what variables are being captured by just using them in the lambda body.


> You state what variables are being captured by just using them in the lambda body.

Wow, have you never spent a week debugging a JavaScript memory leak?


No. What I have done on the other hand was add unused lexical variables to an anonymous function so the runtime wouldn't optimise them out of the closure and I could still see them in the debugger.


I'm not 100% sure, but C#'s compiler should automatically capture what you need (and leave out the rest).

I think the primary need for manual declaration is because in C++ you need to differentiate between pass by copy semantics and pass by reference semantics.


> I think the primary need for manual declaration is because in C++ you need to differentiate between pass by copy semantics and pass by reference semantics.

That's not actually a need, C++ includes [=] and [&] (capture everything by value or by reference). You can get a mix by creating references outside the body then capturing the environment by value (capturing the references by value and thus getting references).

On the one hand it has a bit more syntactic overhead (you have to take and declare a bunch of references before creating the closure), on the other hand there's less irregularity to the language, and bindings mean the same thing in and out of the closure.

FWIW that's what Rust does[0], though it may help that Rust's blocks are "statement expressions", some constructs would probably be unwieldy without that.

[0] the default corresponds to C++'s [&] (capture by ref), and a "move closure" switches to [=] instead


Yep, in Microsoft’s C# compiler, only the closed over local variables of a function are captured (which, in C#’s case, means generating a class with fields corresponding to each closed over local, and then replacing those locals with references to their respective fields of an instance of that class).


PHP also doesn't capture variables by default (except $this) and i like it that way.


The only thing I find somewhat frustrating about the syntax is that the notation messes with my existing expectations. Up until now, in C-like languages a [] was just for collections and indexing into them, in the code I used at least.

I mean I'm not really complaining; I don't see better syntax to fit short anonymous functions into the existing syntax, without defeating the whole purpose of it either.

I suspect it's just a matter of getting used to this extra meaning for angular brackets.


Very often the capture list is empty, it could have been elided (as the parameter list can be) if the syntax could have been made unambiguous.


Well, you need something to indicate the beginning of a lambda. So you can think of "[]" as serving that role, instead of "lambda" in Python or "\" in Haskell.


You can use C# fat arrow syntax, it even allows removing the braces for single expression lambdas which is the most common from anyway.


> removing the braces

If I have learned anything over the years, it's that removing the braces anywhere is the introduction of a bug during a rewrite waiting to happen.


It really works without any issues in C# from my experience.

Statemends like :

    list.Where(e => e.Property == ExpectedValue).Select(e => e.Property)
is much cleaner than something like :

    list.Where([](auto e) { return e.Property == ExpectedValue; }).Select([](auto e) { return e.Property; });
Braces and capture declaration adds 0 value here and it's ~80% of the use cases I see for lambdas.


For historical interest, compare an early proposal: http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2006/n195...


What would you propose as the syntax?


Apple extended C with block closures years ago.

  int b = 0;

  ^(int a) {
    return a*b;
  }
The declaration for lambda variables is almost identical to function pointers, just with a ^ instead of a *, so there's nothing to learn (or unlearn, like C++ forces you to). The ^ looks like a lambda, and historically the lambda of lambda calculus actually was a caret accent over the variable. The argument list can be elided.


Looking here (https://developer.apple.com/library/ios/documentation/Cocoa/...) for the details, I think this is going to largely be a matter of opinion. I prefer the C++ syntax, especially when it comes to capture by reference, for which the Apple syntax seems to require the __block storage type modifier.


And this is why C++ couldn't use this notation, as they didn't want to break compability with this extension of Apple's.


Caret as the embryonic form of lambda is apparently a myth propagated by Barendregt, and lambda is just a random Greek letter to go with alpha, beta, and eta.

http://researchblogs.cs.bham.ac.uk/thelablunch/2016/05/why-i...


I honestly would have much preferred they had a keyword so that it kinda matched the rest of the language. It feels a bit tacked on and hard to parse.

Something like:

lambda(arguments):capture list {...}

Just seems way more clear. Looks more like a function or a class (and a lambda is sort of an in-between kind of thing anyway)


Agreed. From the template library on down, it seems like the C++ community is hellbent on making the syntax for what should be clean, common operations seem like arcane Sanskrit. I dont know what their problem is.


Often, the problem is that the clean simple syntax you might want to use already means something else in C++, and the bias against breaking existing code is very strong.


Refusing to break backwards compatibility is their problem. I respect them for that; if you do want to break it make another language that plays nicely with C++ instead.




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

Search: