Dynamically-Allocated Implementation-Class std::async-ing its Member

62 views Asked by At

Consider an operation with a standard asynchronous interface:

std::future<void> op();

Internally, op needs to perform a (variable) number of asynchronous operations to complete; the number of these operations is finite but unbounded, and depends on the results of the previous asynchronous operations.

Here's a (bad) attempt:

/* An object of this class will store the shared execution state in the members;
*    the asynchronous op is its member. */
class shared
{
private:
    // shared state

private:
    // Actually does some operation (asynchronously).
    void do_op()
    {
        ...
        // Might need to launch more ops.
        if(...)
            launch_next_ops();
    }

public:
    // Launches next ops
    void launch_next_ops()
    {
        ...
        std::async(&shared::do_op, this);
    }
}

std::future<void> op()
{
    shared s;
    s.launch_next_ops();
    // Return some future of s used for the entire operation.
    ...
    // s destructed - delayed BOOM!
};

The problem, of course, is that s goes out of scope, so later methods will not work.

To amend this, here are the changes:

class shared : public std::enable_shared_from_this<shared> 
{
private:
    /* The member now takes a shared pointer to itself; hopefully 
    *    this will keep it alive. */
    void do_op(std::shared_ptr<shared> p); // [*]

    void launch_next_ops()
    {
        ...
        std::async(&shared::do_op, this, shared_from_this());
    }
}

std::future<void> op()
{
    std::shared_ptr<shared> s{new shared{}};
    s->launch_next_ops();
    ...
};

(Asides from the weirdness of an object calling its method with a shared pointer to itself, )the problem is with the line marked [*]. The compiler (correctly) warns that it's an unused variable.

Of course, it's possible to fool it somehow, but is this an indication of a fundamental problem? Is there any chance the compiler will optimize away the argument and leave the method with a dead object? Is there a better alternative to this entire scheme? I don't find the resulting code the most intuitive.

1

There are 1 answers

1
ecatmur On BEST ANSWER

No, the compiler will not optimize away the argument. Indeed, that's irrelevant as the lifetime extension comes from shared_from_this() being bound by decay-copy ([thread.decaycopy]) into the result of the call to std::async ([futures.async]/3).

If you want to avoid the warning of an unused argument, just leave it unnamed; compilers that warn on unused arguments will not warn on unused unnamed arguments.

An alternative is to make do_op static, meaning that you have to use its shared_ptr argument; this also addresses the duplication between this and shared_from_this. Since this is fairly cumbersome, you might want to use a lambda to convert shared_from_this to a this pointer:

std::async([](std::shared_ptr<shared> const& self){ self->do_op(); }, shared_from_this());

If you can use C++14 init-captures this becomes even simpler:

std::async([self = shared_from_this()]{ self->do_op(); });