In the previous post, I gave a brief intro to the PPL and the basic constructs for writing async C++ code. In this post, I will discuss the highlights of how I ported the InputQueue sample to C++ using the PPL and TDD. The code is available on GitHub: NativeQueueSample commit history.
My C++ TDD tool of choice is the Microsoft Unit Testing Framework for C++. The main selling point is its smooth integration with Visual Studio — even the Express edition. You just create a Native Unit Test Project, create a wrapping TEST_CLASS and some TEST_METHOD functions and you’re done. Note that with the Express edition, you cannot set the tests to run automatically after each build. However, you can do as I did and create a Post-Build Event to run the tests. You won’t get the nice integration with Test Explorer but I like it better than manually triggering tests. The command line should look something like this: "$(VSInstallDir)Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe" "$(TargetPath)" /Platform:x64 /inIsolation
The first thing to watch out for when writing unit tests for async C++ code: don’t double fault! I learned this the hard way when the test runner inexplicably hung after running my very first test:
TEST_METHOD(Dequeue_completes_after_enqueue) { InputQueue<wstring> queue; task<wstring> task = queue.DequeueAsync(); Assert::IsFalse(task.is_done()); queue.Enqueue(wstring(L"a")); Assert::IsTrue(task.is_done()); Assert::AreEqual(wstring(L"a"), task.get()); }
Seems simple enough. However, my implementation code, being just a stub, looked like this:
void Enqueue(T item) { throw std::runtime_error("Not implemented."); } concurrency::task<T> DequeueAsync() { return concurrency::task_from_exception<T>(runtime_error("Not implemented.")); }
It turns out that the first Assert::IsFalse
was failing (expected, since I’m completing DequeueAsync
immediately with an exception), which ended up throwing an exception (expected for any assertion failure), which ended up unwinding the stack and destroying the task
object, which ended up failing due to the fact that the original exception was unobserved. To avoid this, I came up with an AssertTaskPending
helper:
template <typename T> task<T> & AssertTaskPending(task<T> & task) { if (task.is_done()) { // Before asserting, force rethrow of exception if task finished with error. // Otherwise, we run the risk of a "double fault" due to an unobserved task // error, which can end up hanging the VS test executor. Logger::WriteMessage(L"Task completed unexpectedly."); task.get(); } Assert::IsFalse(task.is_done()); return task; }
Ensuring that we call get()
before asserting will avoid the issue.
The unit tests ended up looking quite similar to the C# implementation. The actual implementation code, however, had a few minor differences worth pointing out.
To properly represent the case where there is no pending dequeue operation, I needed to make it possible to have a null
value for the task_completion_event
. In modern C++, this suggests the use of smart pointers — specifically, unique_ptr
since there is only a single owner of the resource (the InputQueue
itself).
C++ has deterministic destruction. Unlike the C# implementation where we had to implement IDisposable
and do some handling of still-active tasks, the chained destruction of InputQueue
‘s task_completion_event
conveniently takes care of all of this automatically. Note that tasks that are canceled in this fashion throw task_canceled
.
Despite these minor details, InputQueue
in C++ is largely identical to its managed counterpart. In the next post, I will talk about some thread-safety considerations and the integration test.