Skip to content

Conjunctions

Conjunctions

Like in C++ Extensions for Concurrency, the when_all function is defined for task conjunctions. Say we want to execute the following sequence of asynchronous tasks:

graph LR subgraph Async A --> B A --> C B --> D C --> D end Main --> A D --> End Main --> End

Achieving that with the function when_all is as simple as:

shared_cfuture<int> A = async([]() { return 2; }).share();

cfuture<int> B = then(A, [](int a) { return a * 3; });
cfuture<int> C = then(A, [](int a) { return a * 2; });

cfuture<int> D = then(when_all(B, C), [](int b, int c) {
    return b + c;
});

assert(D.get() == 10);

The function when_all returns a when_all_future that is a future adaptor able to aggregate different futures types and become ready when all internal futures are ready.

auto f1 = async([]() { return 2; });
auto f2 = async([]() { return 3.5; });
auto f3 = async([]() -> std::string { return "name"; });
auto all = when_all(f1, f2, f3);

When retrieving results, a tuple with the original future objects is returned.

auto [r1, r2, r3] = all.get(); // get ready futures
assert(r1.get() == 2);
assert(r2.get() == 3.5);
assert(r3.get() == "name");

When a range is provided to when_all, another range is returned. The when_all_future object acts as a proxy object that checks the state of each internal future. If any of the internal futures isn't ready yet, is_ready returns false.

std::vector<cfuture<int>> fs;
fs.emplace_back(async([]() { return 2; }));
fs.emplace_back(async([]() { return 3; }));
fs.emplace_back(async([]() { return 4; }));
auto all = when_all(fs);

auto rs = all.get();
assert(rs[0].get() == 2);
assert(rs[1].get() == 3);
assert(rs[2].get() == 4);

Operators

The operator && is defined as a convenience to create future conjunctions in large task graphs.

auto f1 = async([]() { return 2; });
auto f2 = async([]() { return 3.5; });
auto f3 = async([]() -> std::string { return "name"; });
auto all = f1 && f2 && f3;

With tuple unwrapping, this becomes a powerful tool to manage continuations:

auto f4 = then(all, [](int a, double b, std::string c) {
    assert(a == 2);
    assert(b == 3.5);
    assert(c == "name");
});

Note that the operator && uses expression templates to create a single conjunction of futures. Thus, f1 && f2 && f3 is equivalent to when_all(f1, f2, f3) rather than when_all(when_all(f1, f2), f3).

The operator && can also be used with lambdas as an easy way to launch new tasks already into conjunctions:

auto f1 = []() { return 2; } &&
          []() { return 3.5; } &&
          []() -> std::string { return "name"; };

auto f2 = then(f1, [](int a, double b, std::string c) {
    assert(a == 2);
    assert(b == 3.5);
    assert(c == "name");
});

This makes lambdas a first class citizen when composing task graphs. The types accepted by these operators only participate in overload resolution if they match the future concept or are callables that are valid new asynchronous tasks. This avoids conflicts with operator overloads defined for other types.

Conjunction unwrapping

The tuple unwrapping functions and especially double unwrapping are especially useful for when_all_future continuations.

auto f1 = async([]() { return 1; });
auto f2 = async([]() { return 2; });
auto f3 = async([]() { return 3; });
auto f4 = async([]() { return 4; });
auto f5 = when_all(f1, f2, f3, f4);
auto f6 = f5 >> [](int a, int b, int c, int d) {
    return a + b + c + d;
};
assert(f6.get() == 1 + 2 + 3 + 4);