Search code examples
c++multithreadingcallablecallable-object

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.


Solution

  • 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.