Boost asio post with shared ptr passed as argument with std::move

417 views Asked by At

I am new to boost:asio. I need to pass shared_ptr as argument to handler function.

E.g.

  1. boost::asio::post(std::bind(&::function_x, std::move(some_shared_ptr)));

Is using std::move(some_shared_ptr) correct? or should I use as below,

  1. boost::asio::post(std::bind(&::function_x, some_shared_ptr));

If both are correct, which one is advisable?

Thanks in advance

Regards Shankar

1

There are 1 answers

0
sehe On

Bind stores arguments by value.

So both are correct and probably equivalent. Moving the argument into the bind is potentially more efficient if some_argument is not gonna be used after the bind.

Warning: Advanced Use Cases

(just skip this if you want)

Not what you asked: what if function_x took rvalue-reference arguments?

Glad you asked. You can't. However, you can still receive by lvalue reference and just move from that. because:

std::move doesn't move

The rvalue-reference is only there to indicate potentially-moved-from arguments enabling some smart compiler optimizations and diagnostics.

So, as long as you know your bound function is only executed once (!!) then it's safe to move from lvalue parameters.

In the case of shared-pointers there's actually a little bit more leeway, because moving from the shared-ptr doesn't actually move the pointed-to element at all.

So, a little exercise demonstrating it all:

Live On Coliru

#include <boost/asio.hpp>
#include <memory>
#include <iostream>

static void foo(std::shared_ptr<int>& move_me) {
    if (!move_me) {
        std::cout << "already moved!\n";
    } else {
        std::cout << "argument: " << *std::move(move_me) << "\n";
        move_me.reset();
    }
}

int main() {

    std::shared_ptr<int> arg      = std::make_shared<int>(42);
    std::weak_ptr<int>   observer = std::weak_ptr(arg);

    assert(observer.use_count() == 1);

    auto f = std::bind(foo, std::move(arg));

    assert(!arg);                      // moved
    assert(observer.use_count() == 1); // so still 1 usage

    {
        boost::asio::io_context ctx;
        post(ctx, f);
        ctx.run();
    }

    assert(observer.use_count() == 1); // so still 1 usage
    f(); // still has the shared arg

     // but now the last copy was moved from, so it's gone
    assert(observer.use_count() == 0); //
    f(); // already moved!
}

Prints

argument: 42
argument: 42
already moved!

Why Bother?

Why would you care about the above? Well, since in Asio you have a lot of handlers that are guaranteed to execute precisely ONCE, you can sometimes avoid the overhead of shared pointers (the synchronization, the allocation of the control block, the type erasure of the deleter).

That is, you can use move-only handlers using std::unique_ptr<>:

Live On Coliru

#include <boost/asio.hpp>
#include <memory>
#include <iostream>

static void foo(std::unique_ptr<int>& move_me) {
    if (!move_me) {
        std::cout << "already moved!\n";
    } else {
        std::cout << "argument: " << *std::move(move_me) << "\n";
        move_me.reset();
    }
}

int main() {
    auto arg = std::make_unique<int>(42);
    auto f = std::bind(foo, std::move(arg)); // this handler is now move-only

    assert(!arg); // moved

    {
        boost::asio::io_context ctx;
        post(
            ctx,
            std::move(f)); // move-only, so move the entire bind (including arg)
        ctx.run();
    }

    f(); // already executed
}

Prints

argument: 42
already moved!

This is going to help a lot in code that uses a lot of composed operations: you can now bind the state of the operation into the handler with zero overhead, even if it's bigger and dynamically allocated.