0

Here are two functions. They are asynchronous functions. Both have a callback function parameter cb. The function template_cb 's callback type is template Cb. The function std_function_cb 's callback type is std::function<void()>.

namespace as = boost::asio;

// BEGIN 3rd party code (I can't modify)
inline
void std_function_cb(as::io_context& ioc, std::function<void()> cb) {
    // pseudo implementation
    as::post(ioc, std::move(cb));
}
// END 3rd party code (I can't modify)

Let's assume we can't modify the functions.

Now, I want to adapt the functions to Boost.Asio CompletionToken. See https://www.boost.org/doc/libs/1_80_0/doc/html/boost_asio/reference/async_compose.html

The current version of the functions can only be used with callback functions. But if I adapt the function to the CompletionToken, they can be worked with not only callback functions, but also futures and coroutines. It flexible.

The following code demonstrates adapted function async_call.

int main() {
    std::cout << BOOST_VERSION << std::endl;
    {
        std::cout << "token as callback" << std::endl;
        as::io_context ioc;
        async_call(
            ioc,
            [] {
                std::cout << "cb called" << std::endl;
            }
        );
        ioc.run();
    }
    {
        std::cout << "token as future" << std::endl;
        as::io_context ioc;
        std::future<void> f = async_call(ioc, as::use_future);
        std::thread th {[&] { ioc.run(); }};
        f.get();
        std::cout << "future unblocked" << std::endl;
        th.join();
    }
}

So I tried it.

I wrote the following adaptation code. But I got compile error.

You can see the complete code and compile error message at https://godbolt.org/z/qxYfGM7hd

It seems that the error is caused by self is movable but not copyable. However, std::function requires copyable.

struct async_impl {
    as::io_context& ioc_;
    enum { first, second } state_ = first;

    template <typename Self>
    void operator()(Self& self) {
        switch (state_) {
        case first:
            state_ = second;
            std_function_cb(ioc_, std::move(self));
            break;
        case second:
            self.complete();
            break;
        }
    }
};

template <typename CompletionToken>
auto async_call(
    as::io_context& ioc,
    CompletionToken&& token
)
-> 
typename as::async_result<
    typename std::decay<CompletionToken>::type,
    void()
>::return_type {
    return 
        as::async_compose<
            CompletionToken,
            void()
        >(async_impl{ioc}, token);
}

I've checked move_only_function and unique_function. I guess they work well with CompletionToken adaptor code. However, I can't modify std_function_cb parameter type.

Is there any way to solve this problem?

Takatoshi Kondo
  • 3,111
  • 17
  • 36
  • I'm missing the point of the question. If `template_cb` works, what is the added value of forcing it to work with `std_function_cb` instead? – sehe Oct 04 '22 at 15:11
  • I didn't explain about that. std_function_cb is given by third party. I can't modify callback type. I thought it is enough to describe my restriction. But I try to explain why std_function_cb has std::function type. std_function_cb might store cb into the internal queue. The queue needs to fixed type. e.g. std::queue>. So std_function_cb 's callback type is std::function type. Even if the function interface could modify to the template, the issue still exist in the body of std_function_cb. – Takatoshi Kondo Oct 05 '22 at 04:28
  • Do you mean that, contrary to the code you provide, `template_cb` doesn't actually exist in your 3rd-party code? You should delete it then. – sehe Oct 05 '22 at 09:04
  • I updated the question based on your suggestion. – Takatoshi Kondo Oct 05 '22 at 09:11

1 Answers1

0

I found a solution.

Replace

            std_function_cb(ioc_, std::move(self));

with

            std_function_cb(
                ioc_, 
                [spself = std::make_shared<Self>(std::move(self))] {
                    (*spself)();
                }
            );

then works well with std_function_cb.

The problem was self is NOT copyable but std::function requires copyable. So somehow making self copyable, then problem is solved. I introduced shared_ptr to wrap self. The wrapped variable is spself (shared_ptr of self). Then passing a lambda function to meet callback signature and capture spself to keep its lifetime during async operation.

My solution requires memory allocation because I use shared_ptr. I guess that it is not avoidable but if better solution exists, I'd like to know.

Complete code is https://godbolt.org/z/3b7ro6nxa

Takatoshi Kondo
  • 3,111
  • 17
  • 36
  • 1
    Mmm. I hope the pseudo code (posting to an execution context) is not actually what is being done, because type-erasing the handler type like this will break executor-associated handlers in ASIO (as well as allocator-associated handlers, but that's a performance problem, less than a correctness problem). See e.g. https://stackoverflow.com/questions/71475533/boostasiobind-executor-does-not-execute-in-strand/71475865#71475865 – sehe Oct 05 '22 at 09:07
  • Thanks. I read the suggested link. I understand that if the library accepts user's handle chain that is associated to the specific executor (e.g. strand) , std::function cannot be used because information is lost. I updated example1 https://godbolt.org/z/PnxPT7xGK line 57-67 would help to understand what I mean. If I want to store the handle (e.g. queing) type erased callable is required but std::function is not a good choice. I should use ErasedHandler (and Afterthoughts function call operator if I need). Am I understand correctly? – Takatoshi Kondo Oct 05 '22 at 10:33
  • 1
    The comment above (by me) is not good. Finally, I understand that if the function has the parameter `std::function` as callback, users shouldn't pass executor binded handler. Users need to assume that executor is ignored. If users want to execute the body of callback on the specific executor, then capture the executor (when the callback function is a lambda expression) and call boost::asio::dispatch with the executor in the body of lambda expression. – Takatoshi Kondo Oct 08 '22 at 00:03
  • Yup. I'd summarize it as that too. The elegance of Asio is that it can be optimized using the associated-executor information, but it comes at the cost of interface complexity that not all the users might understand. The best choice(s) depend on the target audience for that reason. – sehe Oct 08 '22 at 11:50