A library for programming with effect handlers in C++
Effect handlers allow for programming with user-defined computational effects, with applications including custom lightweight concurrency (threads, async-await, actors, generators), error handling, dependency injection, etc. Effect handlers originate from the realm of functional programming, and the main goal of this highly experimental library is to explore how they fit in the more object-oriented setting of C++.
The library relies on modern C++ features (move semantics, variadic templates, compile-time evaluation) to achieve elegant programmer-level interface, memory management of handlers, and relative type-safety. Internally, it uses the boost.context library for call-stack manipulation, and so it implements one-shot handlers only.
-
Reference - A detailed explanation of the library's API and a short discussion about the overall design of the library.
-
High-level effect handlers in C++ - An accompanying research paper (OOPSLA 2022).
As a sneak preview, we can use effect handlers to define our own tiny library for cooperative lightweight threads. The programmer's interface will consist of two functions, yield
and fork
, together with a class that implements a scheduler:
void yield(); // Used by a thread to give up control
void fork(std::function<void()> proc); // Start a new thread
class Scheduler {
public:
static void Start(std::function<void()> f);
};
The static member function Start
initiates the scheduler with f
as the body of the first thread. It returns when all threads finish their jobs.
To implement this interface, we first define two commands, which are data structures used for transferring control from the client code to the handler. We implement yield
and fork
to invoke these commands.
#include "cpp-effects/cpp-effects.h"
namespace eff = cpp_effects;
struct Yield : eff::command<> { };
struct Fork : eff::command<> {
std::function<void()> proc;
};
void yield()
{
eff::invoke_command(Yield{});
}
void fork(std::function<void()> proc)
{
eff::invoke_command(Fork{{}, proc});
}
We define the scheduler, which is a handler that can interpret the two commands by pushing the resumptions (i.e., captured continuations) to the queue.
using Res = eff::resumption<void()>;
class Scheduler : public eff::flat_handler<void, Yield, Fork> {
public:
static void Start(std::function<void()> f)
{
queue.push_back(eff::wrap<Scheduler>(f));
while (!queue.empty()) {
auto resumption = std::move(queue.front());
queue.pop_front();
std::move(resumption).resume();
}
}
private:
static std::list<Res> queue;
void handle_command(Yield, Res r) override
{
queue.push_back(std::move(r));
}
void handle_command(Fork f, Res r) override
{
queue.push_back(std::move(r));
queue.push_back(eff::wrap<Scheduler>(f.proc));
}
};
std::list<Res> Scheduler::queue;
And that's all it takes! We can now test our library by starting a few threads:
void worker(int k)
{
for (int i = 0; i < 10; ++i) {
std::cout << k;
yield();
}
}
void starter()
{
for (int i = 0; i < 5; ++i) {
fork(std::bind(worker, i));
}
}
int main()
{
Scheduler::Start(starter);
// Output:
// 01021032104321043210432104321043210432104321432434
}
- Language: C++17
- Handlers: deep, one-shot, stateful
The library is header-only, so to use it just include the headers and link with boost.context. On most systems, boost is available via a package manager, e.g.,
-
macOS:
brew install boost
-
Ubuntu:
apt-get install libboost-context-dev
You can link with boost using cmake as follows. In your CMakeLists.txt
, use the following:
FIND_PACKAGE (Boost 1.70 COMPONENTS context REQUIRED)
if (Boost_FOUND)
link_libraries (Boost::context)
add_executable (my_program my_program.cpp)
else()
message (STATUS "Boost not found!")
endif()
This repository contains some examples, tests, and benchmarks. The easiest way to build these is to use cmake
. You will need cmake
and boost
in any non-ancient versions. For example, the following should do the trick on macOS:
$ brew install cmake
$ brew install boost
$ cmake .
$ make
You can verify that the build was successful by running an example. The following will run the threads
example - you can see the interleaving of threads in the output:
$ bin/threads
01021032104321043210432104321043210432104321432434
You can also compile and run the examples in a Docker container. Just type in the following to build and then run the container shell:
$ sudo docker build -t cppeff .
$ docker run -it --rm -v $(pwd):/home cppeff
In the container shell type cmake .
and then make
.