c++c++17boost-asiocompletionhandler

How to check a valid boost asio CompletionToken?


I'm writing Boost.Asio style async function that takes CompletionToken argument. The argument can be function, function object, lambda expression, future, awaitable, etc.

CompletionToken is template parameter. If I don't restrict the argument, the function could match unexpected parameter such as int. So I want to write some restriction using std::enable_if. (My environment is C++17).

If CompletionToken takes parameters, then I can use std::is_invocable for checking.

See f1 in my example code.

However, I got a problem. If CompletionToken takes no parameters, then boost::asio::use_future makes an error.

See f2 in my example code.

After some of try and error, I got my solution. That is concatenating std::is_invocable and use_future_t checking by OR (||).

See f3 in my example code.

But it is not so elegant. In addition, I'm not sure other features supported by Boost.Asio e.g.) use_awaitable_t requires similar direct matching check.

I tried to find Boost.Asio provides type traits or predicate such as is_completion_token, but I couldn't find it.

Is there any better way to checking CompletionToken?

Godbolt link https://godbolt.org/z/sPeMo1GEK

Complete Code:

#include <type_traits>
#include <boost/asio.hpp>

// Callable T takes one argument
template <
    typename T,
    std::enable_if_t<std::is_invocable_v<T, int>>* = nullptr
>
void f1(T) {
}

// Callable T takes no argument
template <
    typename T,
    std::enable_if_t<std::is_invocable_v<T>>* = nullptr
>
void f2(T) {
}


template <template <typename...> typename, typename>
struct is_instance_of : std::false_type {};

template <template <typename...> typename T, typename U>
struct is_instance_of<T, T<U>> : std::true_type {};

// Callable T takes no argument
template <
    typename T,
    std::enable_if_t<
        std::is_invocable_v<T> ||
        is_instance_of<boost::asio::use_future_t, T>::value
    >* = nullptr
>
void f3(T) {
}

int main() {
    // no error
    f1([](int){});
    f1(boost::asio::use_future);

    // same rule as f1 but use_future got compile error
    f2([](){});
    f2(boost::asio::use_future); // error

    // a little complecated typechecking, then no error
    f3([](){});
    f3(boost::asio::use_future);
}

Outputs:

Output of x86-64 clang 13.0.1 (Compiler #1)
<source>:45:5: error: no matching function for call to 'f2'
    f2(boost::asio::use_future); // error
    ^~
<source>:17:6: note: candidate template ignored: requirement 'std::is_invocable_v<boost::asio::use_future_t<std::allocator<void>>>' was not satisfied [with T = boost::asio::use_future_t<>]
void f2(T) {
     ^
1 error generated.

Solution

  • If you have c++20 concepts, look below. Otherwise, read on.

    When you want to correctly implement the async-result protocol using Asio, you would use the async_result trait, or the async_initiate as documented here.

    This should be a reliable key for SFINAE. The template arguments to async_result include the token and the completion signature(s):

    Live On Compiler Explorer

    #include <boost/asio.hpp>
    #include <iostream>
    using boost::asio::async_result;
    
    template <typename Token,
              typename R = typename async_result<std::decay_t<Token>, void(int)>::return_type>
    void f1(Token&&) {
        std::cout << __PRETTY_FUNCTION__ << "\n";
    }
    
    template <typename Token,
              typename R = typename async_result<std::decay_t<Token>, void()>::return_type>
    void f2(Token&&) {
        std::cout << __PRETTY_FUNCTION__ << "\n";
    }
    
    int main() {
        auto cb1 = [](int) {};
        f1(cb1);
        f1(boost::asio::use_future);
        f1(boost::asio::use_awaitable);
        f1(boost::asio::detached);
        f1(boost::asio::as_tuple(boost::asio::use_awaitable));
    
        auto cb2 = []() {};
        f2(cb2);
        f2(boost::asio::use_future);
        f2(boost::asio::use_awaitable);
        f2(boost::asio::detached);
        f2(boost::asio::as_tuple(boost::asio::use_awaitable));
    }
    

    Already prints

    void f1(Token&&) [with Token = main()::<lambda(int)>&; R = void]
    void f1(Token&&) [with Token = const boost::asio::use_future_t<>&; R = std::future<int>]
    void f1(Token&&) [with Token = const boost::asio::use_awaitable_t<>&; R = boost::asio::awaitable<int, boost::asio::any_io_executor>]
    void f1(Token&&) [with Token = const boost::asio::detached_t&; R = void]
    void f1(Token&&) [with Token = boost::asio::as_tuple_t<boost::asio::use_awaitable_t<> >; R = boost::asio::awaitable<std::tuple<int>, boost::asio::any_io_executor>]
    void f2(Token&&) [with Token = main()::<lambda()>&; R = void]
    void f2(Token&&) [with Token = const boost::asio::use_future_t<>&; R = std::future<void>]
    void f2(Token&&) [with Token = const boost::asio::use_awaitable_t<>&; R = boost::asio::awaitable<void, boost::asio::any_io_executor>]
    void f2(Token&&) [with Token = const boost::asio::detached_t&; R = void]
    void f2(Token&&) [with Token = boost::asio::as_tuple_t<boost::asio::use_awaitable_t<> >; R = boost::asio::awaitable<std::tuple<>, boost::asio::any_io_executor>]
    

    C++20 Concepts

    Now keep in mind the above is "too lax" due to partial template instantiation. Some parts of async_result aren't actually used. That means that f2(cb1); will actually compile.

    The linked docs even include the C++20 completion_token_for<Sig> concept that allows you to be precise at no effort: Live On Compiler Explorer

    template <boost::asio::completion_token_for<void(int)> Token> void f1(Token&&) {
        std::cout << __PRETTY_FUNCTION__ << "\n";
    }
    
    template <boost::asio::completion_token_for<void()> Token> void f2(Token&&) {
        std::cout << __PRETTY_FUNCTION__ << "\n";
    }
    

    Otherwise

    In practice you would always follow the Asio recipe, and that guarantees that all parts are used. Apart from the example in the documentation, you can search existing answers

    Example:

    template <typename Token>
    typename asio::async_result<std::decay_t<Token>, void(error_code, int)>::return_type
    async_f1(Token&& token) {
        auto init = [](auto completion) {
            auto timer =
                std::make_shared<asio::steady_timer>(boost::asio::system_executor{}, 1s);
            std::thread(
                [timer](auto completion) {
                    error_code ec;
                    timer->wait(ec);
                    std::move(completion)(ec, 42);
                },
                std::move(completion))
                .detach();
        };
    
        return asio::async_result<std::decay_t<Token>, void(error_code, int)>::initiate(
            init, std::forward<Token>(token));
    }