Search code examples
c++algorithmc++-coroutine

Is it possible to combine coroutines and templates from `<algorithm>` header?


I write a lot of TCP/IP based C++ software, and I use modern C++ coroutines for network communications. Now let suppose I have array of URLs, and I want to find which URL downloads document that contains "Hello" string:

vector<string> my_urls = { /* URLs list here */ };
auto hello_iterator = find_if(my_urls.begin(), my_urls.end(), [](const string &url) 
{ 
    string downloaded_data = download(url);
    return downloaded_data.find("Hello") != string::npos;
});

Here we use synchronous download(const std::string& url) function to download data for each URL.

With coroutines I want to do something similar:

vector<string> my_urls = { /* URLs list here */ };
auto hello_iterator = find_if(my_urls.begin(), my_urls.end(), [](const string &url) -> MyPromiseClass
{ 
    string downloaded_data = co_await async_download(url);
    return downloaded_data.find("Hello") != string::npos;
});

I have MyPromiseClass async_download(const std::string& url) that works nice, and I want to use it to download data asynchronously.

But such code doesn't compile. In Visual C++ I have following error:

error C2451: a conditional expression of type 'MyPromiseClass' is not valid

The reason is that standard find_if algorithm "doesn't know" about coroutines and simply tries to convert MyPromiseClass to bool.

I however can easily implement coroutine version of find_if and/or any other standard algorithm that will work by just changing its if statement to one that uses co_await when calls predicate and returns promise instead of iterator, so I hope that C++ standard should also contain similar algorithms?

Please advise are there any version of <algorithm> header in C++ standard or boost that supports coroutines, or are there any way to easily convert "old" algorithms from <aglorithm> header to support coroutines without manual rewriting them or ugly code that first precalculates values (with coroutines) and later uses algorithms on these precalculated values instead of just awaiting data in lambda expression?


Solution

  • Only coroutines can call co_await, but the standard algorithms aren't coroutines (and one can argue that they shouldn't be). This means that you can't pass a coroutine into a standard algorithm and expect it to wait for its result.

    If the standard algorithms were coroutines, you couldn't just call them and get their result - instead, they'd all return futures or coroutine types that you'd have to wait on before proceeding, similar to how your async_download function doesn't return a std::string directly, but rather some kind of custom future. As a result, the standard algorithms would be really difficult to use in anything but a coroutine. This would be necessary because any coroutines passed into a standard algorithm could suspend themselves, which in turn means that the algorithm itself would have to be able to suspend itself, making it a coroutine.

    Note that a coroutine "suspending" means that the coroutine saves its state to its coroutine frame and then returns. If you need this to work across multiple levels of the call stack, every function in that part of the call stack has to be in on the joke and be able to return early when a coroutine somewhere down the line decides to suspend. Coroutines can trivially do this via co_await, and you can also write code that does this manually by i.e. returning a future.

    Since the standard algorithms return plain values, and not futures, they can't return early and therefore don't support suspension. As a result, they can't call coroutines.

    What you could do instead is to download the data first, and then search for the string:

    std::vector<std::string> urls = ...;
    std::vector<MyPromiseClass> downloads;
    
    //Start downloading everything in parallel
    std::transform(urls.begin(), urls.end(),
                   std::back_insert_iterator(downloads), async_download);
    
    std::vector<std::string> data;
    
    //Wait for all downloads
    for (auto& promise: downloads) {
        data.push_back(co_await promise);
    }
    
    auto hello_iterator = std::find_if(data.begin(), data.end(), ...);
    

    If you wanted to, you could create a helper function (templated coroutine) that co_awaits multiple awaitable objects and returns their results.