I converted Ekam to C++0x. As always, all code is at:
http://code.google.com/p/ekam/
Note that Ekam now requires a C++0x compiler. Namely, it needs GCC 4.6, which is not officially released yet. I didn't have much trouble compiling and using the latest snapshot, but I realize that it is probably more work than most people want to do. Hopefully 4.6 will be officially released soon.
Introduction
When writing Ekam, with no company style guide to stop me, I have found myself developing a very specific and unique style of C++ programming with a heavy reliance on RAII. Some features of this style:
- I never use operator
new
directly (much lessmalloc()
), but instead use a wrapper which initializes anOwnedPtr
. This class is likescoped_ptr
in that it wraps a pointer and automatically deletes it when theOwnedPtr
is destroyed. However, unlikescope_ptr
, there is no way to release a pointer from anOwnedPtr
except by transferring it to anotherOwnedPtr
. Thus, the only way that an object pointed to by anOwnedPtr
could ever be leaked (i.e. become unreachable without being reclaimed) is if you constructed anOwnedPtr
cycle. This is actually quite hard to do by accident -- much harder than creating a cycle of regular pointers. - Ekam is heavily event-driven. Any function call which starts an asynchronous operation returns an
OwnedPtr<AsyncOperation>
. Deleting this object cancels the operation. - All OS handles (e.g. file descriptors) are wrapped in objects that automatically close them.
These features turn out to work extremely well together.
A common problem in multi-tasking C++ code (whether based on threads or events) is that cancellation is very difficult. Typically, an asynchronous operation calls some callback at some future time, and the caller is expected to ensure that the callback's context is still valid at the time that it is called. If you're lucky, the operation can be canceled by calling some separate cancel()
function. However, it's often the case that this function simply causes the callback to complete sooner, because it's considered too easy to leak memory if an expected callback is never called. So, you still have to wait for the callback.
So what happens if you really just want to kill off an entire large, complex chunk of your program all at once? It turns out this is something I need to do in Ekam. If a build action is in progress and one of its inputs changes, the action should be immediately halted. But actions can involve arbitrary code that can get fairly complex. What can Ekam do about it?
Well, with the style I've been using, cancellation is actually quite easy. Because all allocated objects must be anchored to another object via an OwnedPtr
, if you delete a high-level object, you can be pretty sure that all the objects underneath will be cleanly deleted. And because asychronous operations are themselves represented using objects, and deleting those objects cancel the corresponding operations, it's nearly impossible to accidentally leave an operation running after its context has been deleted.
Problem: OwnedPtr
transferral
So what does this have to do with C++0x? Well, there are some parts of my style that turn out to be a bit awkward.
Transferring an OwnedPtr
to another OwnedPtr
looked like this:
OwnedPtr<MyObject> ptr1, ptr2; //... ptr1.adopt(&ptr2);
Looks fine, but this means that the way to pass ownership into a function call is by passing a pointer to an OwnedPtr
, getting a little weird:
void Foo::takeOwnership(OwnedPtr<Bar>* barToAdopt) { this->bar.adopt(barToAdopt); } ... Foo foo; OwnedPtr<Bar> bar; ... foo.takeOwnership(&bar);
Returning an OwnedPtr
is even more awkward:
void Foo::releaseOwnership(OwnedPtr<Bar>* output) { output->adopt(&this->bar); } ... OwnedPtr<Bar> bar; foo.releaseOwnership(&bar); bar->doSomething();
Furthermore, the way to allocate an owned object was through a method of OwnedPtr
itself, which was kind of weird to call:
OwnedPtr<Bar> bar; bar.allocate(constructorParam1, constructorParam2); foo.takeOwnership(&bar);
This turned out to be particularly ugly when allocating a subclass:
OwnedPtr<BarSub> barSub; barSub.allocate(constructorParam1, constructorParam2); OwnedPtr<Bar> bar; bar.adopt(&barSub); foo.takeOwnership(&bar);
So I made a shortcut for that:
OwnedPtr<Bar> bar; bar.allocateSubclass<BarSub>( constructorParam1, constructorParam2); foo.takeOwnership(&bar);
Still, dealing with OwnedPtr
s remained difficult. They just didn't flow right with the rest of the language.
Rvalue references
This is all solved by C++0x's new "rvalue references" feature. When a function takes an "rvalue reference" as a parameter, it only accepts references to values which are safe to clobber, either because the value is an unnamed temporary (which will be destroyed immediately when the function returns) or because the caller has explicitly indicated that it's OK to clobber the value.
Most of the literature on rvalue references talks about how they can be used to avoid unnecessary copies and to implement "perfect forwarding". These are nice, but what I really want is to implement a type that can only be moved, not copied. OwnedPtr
s explicitly prohibit copying, since this would lead to double-deletion. However, moving an OwnedPtr
is perfectly safe. By implementing move semantics using rvalue references, I was able to make it possible to pass OwnedPtr
s around using natural syntax, without any risk of unexpected ownership stealing (as with the old auto_ptr
).
Now the code samples look like this:
// Transferring ownership. OwnedPtr<MyObject> ptr1, ptr2; ... ptr1 = ptr2.release(); // Passing ownership to a method. void Foo::takeOwnership(OwnedPtr<Bar> bar) { this->bar = bar.release(); } ... Foo foo; OwnedPtr<Bar> bar; ... foo.takeOwnership(bar.release()); // Returning ownership from a method. OwnedPtr<Bar> Foo::releaseOwnership() { return this->bar.release(); } ... OwnedPtr<Bar> bar = foo.releaseOwnership(); bar->doSomething(); // Allocating an object. OwnedPtr<Bar> bar = newOwned<Bar>( constructorParam1, constructorParam2); // Allocating a subclass. OwnedPtr<Bar> bar = newOwned<BarSub>( constructorParam1, constructorParam2);
So much nicer! Notice that the release()
method is always used in contexts where ownership is being transfered away from a named OwnedPtr
. This makes it very clear what is going on and avoids accidents. Notice also that release()
is NOT needed if the OwnedPtr
is an unnamed temporary, which allows complex expressions to be written relatively naturally.
Problem: Callbacks
While working better than typical callback-based systems, my style for asynchronous operations in Ekam was still fundamentally based on callbacks. This typically involved a lot of boilerplate. For example, here is some code to implement an asynchronous read, based on the EventManager interface which provides asynchronous notification of readability:
class ReadCallback { public: virtual ~ReadCallback(); virtual void done(size_t actual); virtual void error(int number); }; OwnedPtr<AsyncOperation> readAsync( EventManager* eventManager, int fd, void* buffer, size_t size, ReadCallback* callback) { class ReadOperation: public EventManager::IoCallback, public AsyncOperation { public: ReadOperation(int fd, void* buffer, size_t size, ReadCallback* callback) : fd(fd), buffer(buffer), size(size), callback(callback) {} ~ReadOperation() {} OwnedPtr<AsyncOperation> inner; // implements IoCallback virtual void ready() { ssize_t n = read(fd, buffer, size); if (n < 0) { callback->error(errno); } else { callback->done(n); } } private: int fd; void* buffer; size_t size; ReadCallback* callback; } OwnedPtr<ReadOperation> result = newOwned<ReadOperation>( fd, buffer, size, callback); result.inner = eventManager->onReadable(fd, result.get()); return result.release(); }
That's a lot of code to do something pretty trivial. Additionally, the fact that callbacks transfer control from lower-level objects to higher-level ones causes some problems:
- Exceptions can't be used, because they would propagate in the wrong direction.
- When the callback returns, the calling object may have been destroyed. Detecting this situation is hard, and delaying destruction if needed is harder. Most callback callers are lucky enough not to have anything else to do after the call, but this isn't always the case.
C++0x introduces lambdas. Using them, I implemented E-style promises. Here's what the new code looks like:
Promise<size_t> readAsync( EventManager* eventManager, int fd, void* buffer, size_t size) { return eventManager->when(eventManager->onReadable(fd))( [=](Void) -> size_t { ssize_t n = read(fd, buffer, size); if (n < 0) { throw OsError("read", errno); } else { return n; } }); }
Isn't that pretty? It does all the same things as the previous code sample, but with so much less code. Here's another example which calls the above:
Promise<size_t> readPromise = readAsync( eventManager, fd, buffer, size); Promise<void> pendingOp = eventManager->when(readPromise)( [=](size_t actual) { // Copy to stdout. write(STDOUT_FILENO, buffer, actual); }, [](MaybeExceptionerror) { try { // Force exception to be rethrown. error.get(); } catch (const OsError& e) { fprintf(stderr, "%s\n", e.what()); } })
Some points:
- The return value of
when()
is another promise, for the result of the lambda. - The lambda can return another promise instead of a value. In this case the new promise will replace the old one.
- You can pass multiple promises to
when()
. The lambda will be called when all have completed. - If you give two lambdas to
when()
, the second one is called in case of exceptions. Otherwise, exceptions simply propagate to the lambda returned bywhen()
. - Promise callbacks are never executed synchronously; they always go through an event queue. Therefore, the body of a promise callback can delete objcets without worrying that they are in use up the stack.
when()
takes ownership of all of its arguments (using rvalue reference "move" semantics). You can actually pass things other than promises to it; they will simply be passed through to the callback. This is useful for making sure state required by the callback is not destroyed in the meantime.- If you destroy a promise without passing it to
when()
, whatever asynchronous operation it was bound to is canceled. Even if the promise was already fulfilled and the callback is simply sitting on the event queue, it will be removed and will never be called.
Having refactored all of my code to use promises, I do find them quite a bit easier to use. For example, it turns out that much of the complication in using Linux's inotify
interface, which I whined about a few months ago, completely went away when I started using promises, because I didn't need to worry about callbacks interfering with each other.
Conclusion
C++ is still a horribly over-complicated language, and C++0x only makes that worse. The implementation of promises is a ridiculous mess of template magic that is pretty inscrutable. However, for those who deeply understand C++, C++0x provides some very powerful features. I'm pretty happy with the results.