5 ms·
How is it ugly? The capture list is a necessary complexity in a language with manual memory management.
by hendzen 10y ago
How is it ugly? The capture list is a necessary complexity in a language with manual memory management.
- steveklabnik 10y agoIt'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.
- kibwen 10y agoRust 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.
- hendzen 10y agoHow 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(...); })
- steveklabnik 10y agolet 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.
- hendzen 10y agoWhat 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(....);
- vvanders 10y agoIf 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.
- steveklabnik 10y agoI 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.
- partisan 10y agoThe 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.
- rtpg 10y agoI'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.
- noblethrasher 10y agoYep, 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).
- masklinn 10y ago> 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
- masklinn 10y ago> 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.
- kibwen 10y agoVery 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.
- yongjik 10y agoWell, 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.
- rubber_duck 10y agoYou can use C# fat arrow syntax, it even allows removing the braces for single expression lambdas which is the most common from anyway.
- vanderZwan 10y ago> 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.
- rubber_duck 10y agoIt 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.
- vanderZwan 10y agoThe 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.