Yunsheng Guo
Yunsheng Guo

Reputation: 85

Spawning threads in a thread with callable object

I've seen this problem on multiple occasions, and it seems it occurs in both Windoes(visual studio) and Linux(gcc). Here is a streamlined version of it:

class noncopyable
{
public:
    noncopyable(int n);
    ~noncopyable();
    noncopyable(const noncopyable&) = delete;
    noncopyable& operator=(const noncopyable&) = delete;
    noncopyable(noncopyable&&);
    int& setvalue();
private:
    int* number;
};

class thread_starter
{
public:
    template<typename callable>
    bool start(callable & o);
};

template<typename callable>
inline bool thread_starter::start(callable & o)
{
    std::thread t(
        [&]() {
        int i = 10;
        while (i-- > 0)
        {
            noncopyable m(i);
            std::thread child(o, std::move(m));
            child.detach();
        }
    });
    return true;
}

class callable
{
public:
    virtual void operator()(noncopyable &m);
};

void callable::operator()(noncopyable & m) { m.setvalue()++; }


int main()
{
    thread_starter ts;
    callable o;
    ts.start(o);
}

The code seems legit enough, but It won't compile.

In Visual Studio, it will give:

error C2893: Failed to specialize function template 'unknown-type std::invoke(_Callable &&,_Types &&...) noexcept(<expr>)'

in GCC, it will give:

error: no type named ‘type’ in ‘class std::result_of<callable(int)>’....

I think I know the problem is in some form of copying or referencing mechanism, but all the syntax seems to be proper.

What am I missing?

I've changed the example a little bit and I apologize for the confusion. I'm trying to recreate the problem as pure as possible, and I don't fully understand it myself.

Upvotes: 0

Views: 1607

Answers (2)

h0od
h0od

Reputation: 69

I made a similar program to figure out what happens. This is how it looks:

class mynoncopy 
{
public:

    mynoncopy(int resource)
        : resource(resource)
    {

    }

    mynoncopy(mynoncopy&& other)
        : resource(other.resource)
    {
        other.resource = 0;
    }

    mynoncopy(const mynoncopy& other) = delete;

    mynoncopy& operator =(const mynoncopy& other) = delete;

public:

    void useResource() {}

private:
    int resource;
};

class mycallablevaluearg
{
public:

    void operator ()(mynoncopy noncopyablething)
    {
        noncopyablething.useResource();
    }
};

class mycallableconstrefarg
{
public:

    void operator ()(const mynoncopy& noncopyablething)
    {
        //noncopyablething.useResource(); // can't do this becuase of const :(
    }
};

class mycallablerefarg
{
public:

    void operator ()(mynoncopy& noncopyablething)
    {
        noncopyablething.useResource();
    }
};

class mycallablervaluerefarg
{
public:

    void operator ()(mynoncopy&& noncopyablething)
    {
        noncopyablething.useResource();
    }
};

class mycallabletemplatearg
{
public:

    template<typename T>
    void operator ()(T&& noncopyablething)
    {
        noncopyablething.useResource();
    }
};

When you issue std::thread(callable, std::move(thenoncopyableinstance)) these two things will happen internally using template magic:

  1. A tuple is created with your callable and all the args.
    std::tuple<mycallablerefarg, mynoncopy> thetuple(callable, std::move(thenoncopyableinstance));
    The callable will be copied in this case.

  2. std::invoke() is used to invoke the callable and the arg is passed to it from the tuple using move semantics.
    std::invoke(std::move(std::get<0>(thetuple)), std::move(std::get<1>(thetuple)));

Because move semantics is used it will expect the callable to receive a rvalue reference as an argument (mynoncopy&& in our case). This limits us to the following argument signatures:

  1. mynoncopy&&
  2. const mynoncopy&
  3. T&& where T is a template argument
  4. mynoncopy not a reference (this will call the move-constructor)

These are the compilation results of using the different types of callables:

mynoncopy testthing(1337);

std::thread t(mycallablerefarg(), std::move(testthing)); // Fails, because it can not match the arguments. This is your case.
std::thread t(mycallablevaluearg(), std::move(testthing)); // OK, because the move semantics will be used to construct it so it will basically work as your solution
std::thread t(mycallableconstrefarg(), std::move(testthing)); // OK, because the argument is const reference
std::thread t(mycallablervaluerefarg(), std::move(testthing)); // OK, because the argument is rvalue reference 
std::thread t(mycallabletemplatearg(), std::move(testthing)); // OK, because template deduction kicks in and gives you noncopyablething&&
std::thread t(std::bind(mycallablerefarg(), std::move(testthing))); // OK, gives you a little bit of call overhead but works. Because bind() does not seem to use move semantics when invoking the callable
std::thread t(std::bind(mycallablevalue(), std::move(testthing))); // Fails, because bind() does not use move semantics when it invokes the callable so it will need to copy the value, which it can't.

Upvotes: 1

Jive Dadson
Jive Dadson

Reputation: 17056

The call to std::thread creates a tuple. Tuples cannot be initialized with references. Thus you must use a fake-reference, std::ref(i) to get it to compile and to call the callables with int-refs int&.

template <typename callable>
bool thread_starter::start(callable &o)
{
    // Nonsense
    std::thread t(
        [&]() {
        int i = 10;
        while (i-- > 0)
        {
            std::thread child(o, std::ref(i));
            child.detach();
        }
    });
    return true;
}

However, the resulting code makes no sense. The spawned threads compete with the while loop. The loop decrements the index i, while the threads attempt to increment it. There is no guarantee as to when these things will happen. The increments and decrements are not atomic. A thread might try to increment the index after the lambda finishes.

In short, if you make it compile, the result is undefined behavior for a number of reasons.

What are you actually trying to do?

Upvotes: 2

Related Questions