18

In a RPC communication protocol, after the invocation of a method I'm sending "done" messages back to the caller. Since the methods are invoked in a concurrent fashion, the buffer containing the response (a std::string) needs to be protected by a mutex. What I'm trying to achieve is the following:

void connection::send_response()
{
    // block until previous response is sent
    std::unique_lock<std::mutex> locker(response_mutex_);

    // prepare response
    response_ = "foo";

    // send response back to caller. move the unique_lock into the binder
    // to keep the mutex locked until asio is done sending.
    asio::async_write(stream_,
                      asio::const_buffers_1(response_.data(), response_.size()),
                      std::bind(&connection::response_sent, shared_from_this(),
                                _1, _2, std::move(locker))
                      );
}

void connection::response_sent(const boost::system::error_code& err, std::size_t len)
{
    if (err) handle_error(err);
    // the mutex is unlocked when the binder is destroyed
}

However, this fails to compile, since boost::asio requires handlers to be CopyConstructible.

The problem can be worked around (albeit not very elegantly) by using the following shared locker class instead of unique_lock:

template <typename Mutex>
class shared_lock
{
public:
    shared_lock(Mutex& m)
    : p_(&m, std::mem_fn(&Mutex::unlock))
    { m.lock(); }

private:
    std::shared_ptr<Mutex> p_;
};

What is the reasoning behind boost::asio not allowing move-only handlers?

marton78
  • 3,899
  • 2
  • 27
  • 38
  • 2
    it's probably because it's older than C++11 and they didn't have the time/attention to fix it. See whether it is the case in the most recent `boost` and, if it is, file a bug! – Massa Jun 20 '13 at 10:31
  • 1
    Hi Márton ;) At a first glance I would guess it's merely because it hasn't been updated for C++11 yet, like many other boost libraries. Sad state of affairs :( – R. Martinho Fernandes Jun 20 '13 at 10:32
  • 1
    what compiler and which version of boost? Later versions of Boost.Asio indeed support [movable handlers](http://www.boost.org/doc/libs/1_53_0/doc/html/boost_asio/overview/cpp2011/move_handlers.html). – Sam Miller Jun 20 '13 at 14:38
  • @SamMiller: I use the Clang that comes with Apple's newest Xcode (4.6.2) and Boost 1.53. The very page you link to states the following: "However, handler types are still required to be copy constructible." This is exactly my question: why? – marton78 Jun 24 '13 at 10:36
  • 2
    @Massa I've filed a bug, here it is for reference: https://svn.boost.org/trac/boost/ticket/8714 – marton78 Jun 24 '13 at 10:38

2 Answers2

13

Until Chris Kohlhoff responds to the bug I've filed, here's a simple workaround:

template <typename F>
struct move_wrapper : F
{
    move_wrapper(F&& f) : F(std::move(f)) {}

    move_wrapper(move_wrapper&&) = default;
    move_wrapper& operator=(move_wrapper&&) = default;

    move_wrapper(const move_wrapper&);
    move_wrapper& operator=(const move_wrapper&);
};

template <typename T>
auto move_handler(T&& t) -> move_wrapper<typename std::decay<T>::type>
{
    return std::move(t);
}

The wrapper declares a copy constructor, tricking asio's machinery into submission, but never defines it, so that copying would result in a linking error.

Now one can finally do this:

std::packaged_task<int()> pt([] {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return 42;
});
std::future<int> fu = pt.get_future();

boost::asio::io_service io;
io.post(move_handler(pt));
std::thread(&boost::asio::io_service::run, &io).detach();

int result = fu.get();
assert(result == 42);
marton78
  • 3,899
  • 2
  • 27
  • 38
  • Is there any update on a structural fix for this in Boost? If not, can you post a reference to an open defect? – rustyx Feb 07 '16 at 15:36
  • 2
    look [here](https://github.com/chriskohlhoff/asio/issues/13) and [here](https://github.com/chriskohlhoff/asio/issues/69) – marton78 Feb 07 '16 at 15:41
1

Here's a simpler workaround:

shared_ptr<mutex> lock(mutex & m)
{
    m.lock();
    return shared_ptr<mutex>(&m, mem_fn(&mutex::unlock));
}

No need to write custom wrappers.

Referring to Smart Pointer Programming Techniques you can even use:

class shared_lock    {
private:    
    shared_ptr<void> pv;    
public:    
    template<class Mutex> explicit shared_lock(Mutex & m): pv((m.lock(), &m), mem_fn(&Mutex::unlock)) {}
};

shared_lock can now be used as:

shared_lock lock(m);

Note that shared_lock is not templated on the mutex type, thanks to shared_ptr<void>'s ability to hide type information.

It potentially costs more, but it has some thing going for it too (the receiver can take shared_lock and you could pass it an upgradable, shared, unique lock, scope_guard, of basically any BasicLockable or "better"

sehe
  • 374,641
  • 47
  • 450
  • 633
  • True, this is shorter than my original `shared_lock` wrapper, albeit with more overhead, just as you wrote. Still, the best solution is IMHO to just pretend that your handler is copyable by declaring the copy constructor and the copy assignment operator. Since asio never actually uses them, linking succeeds. – marton78 Jan 30 '15 at 14:56
  • In either version, if the shared_ptr constructor throws (e.g. std::bad_alloc, because of the control block allocation), the mutex will not be unlocked. It would be better to use a unique_lock. I'd like to move that directly into the Deleter, but stupidly and pointlessly, Deleters must be CopyConstructible, so instead: std::unique_lock l(m); shared_ptr result{&m, mem_fun(&Mutex::unlock)}; l.release(); return result; – Arne Vogel May 11 '15 at 12:06
  • @ArneVogel There is no allocation. Look at it? Also in your sketch you're doing the same **but** you unlock the mutex before returning. Did you miss the point of the question entirely? – sehe May 11 '15 at 12:16