Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Keeping track of which lambda owns which captured variables can quickly become impractical, especially for complex capture structures that can create shared and even cyclic ownership in unclear and unexpected ways. For example, you might have a class that has a callback as a member with standard accessors (getter/setter); an object of that class could potentially have been captured by a lambda that is then set as the object's callback, creating a cyclic reference and potentially leaking memory. A lambda could even wind up owning itself if you are not careful. Unlike objects, which have a class definition (or at least a base class definition) that can be used to clarify or enforce ownership, lambdas can be created anywhere -- you may not even have access to the code that created a closure your code takes as a callback.

So while C++ lambdas are useful as anonymous functions and when passed "downward" (if you are very careful about when and how ownership is transferred), the full power of lambda expressions is not really available to a C++ programmer. It is unfortunate that lambdas are so crippled in C++, because lambdas could become the basis for even more powerful language features (pattern matching, continuations, etc.). Of course, the C++ standards committee continues to see lambdas as a kind of shorthand for defining "function objects," rather than as a first-class type that can be used to define more advanced features.

RAII is not all it's cracked up to be. It works for memory management as long as you are careful, but it is a lot less robust for other resources. For example, fstream, a textbook example of RAII, awkwardly forces programmers to explicitly ask for exceptions to be thrown. This is necessary because close can potentially throw an exception, but RAII means that you are calling close in the destructor -- and exceptions cannot safely propagate from destructors because destructors are called as the stack is unwound during exception propagation. This is one example of what I meant when I said C++ features are not compatible with each other.

Ironically, adding a garbage collector could fix RAII by un-crippling lambdas. The idea here is to create something like the conditions/restarts system from Common Lisp (or call-with-current-continuation from Scheme), using lambdas to support CPS conversion and using continuations to implement "exceptions" (i.e. "conditions"). Garbage collection would allow more liberal use of lambdas, and thus the compiler could automatically generate lambdas to implement other language features (just like it generates an array of function pointers to support polymorphism for classes). CPS conversion involves (among other things) taking code appearing below a function call, wrapping in a lambda that captures the stack frame, and passing that lambda as an (implicit) argument to the function (just like "this" is an implicit argument to member functions). "catch" blocks are similarly wrapped in lambdas ("continuations") and passed as arguments, and "throw" statements would call the appropriate exception handling argument ("return" statements call the non-exceptional continuation). Since everything is a tail call, you wind up heap-allocating the "stack frames" and relying on the garbage collector to deallocate everything (at this point it should be clear that explicitly managing ownership in this setting is totally unworkable). Now instead of unwinding the stack you have control flow jumping directly to your exception handlers, which can potentially return control flow to a defined "restart" to avoid unwinding the stack or else unwind the stack at the end of the catch block (unless another exception is thrown, in which case unwinding will be further delayed). Destructor exceptions are no longer a problem, because they can only be thrown when there are no active exceptions (or more precisely, when there would be no ambiguity about which exception is "active").

Sure, C++ wouldn't be C++ if it did what I described above -- unless you had the ability to add a declaration that you want it to happen for certain functions/class methods, for example by having a "collected namespace foo {..." syntax or whatever. It would be a headache for compiler writers to have to manage two separate function call paradigms, but it is not technically impossible to mix CPS code with C-style call stack semantics, and anyway CPS conversion is not the only way to implement what I described. Sadly the C++ standards committee does not see the poor compatibility between features as a problem that needs to be solved, so I doubt anything like this will ever happen.



If you want GC shared_ptr is always available in C++. Sure, cyclic loops are always possible, but it is not something that happens often by mistake. In 15 year of professional work in C++ I've nerver seen them.

On the other hand lambdas would be significantly less useful if any use implied an allocation and GC was forced.

The issue with fstream is that if you really care about the error you call close explicitly, otherwise you let the destructor swallow it.

In fact if you really care about errors and you would use a transactional interface, with an explicit commit and a no-fail implicit rollback in the destructor. RAII works perfectly for that.

Regarding CPS, that's exactly what the new coroutines do. They end up heap allocating the stack frame, which has proven extremely contentious to say the least. Still, RAII works just fine there.




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

Search: