Steve Emmerson
Steve Emmerson

Reputation: 7832

Subclassing std::thread: problems with variadic template functions in constuctor

I'm trying to subclass std::thread such that a member function of the subclass is executed on the new thread before the caller's passed-in function. Something like the following invalid code:

#include <thread>
#include <utility>

class MyThread : public std::thread {
    template<class Func, class... Args>
    void start(Func&& func, Args&&... args) {
        ... // Useful, thread-specific action
        func(args...);
    }
public:
    template<class Func, class... Args>
    MyThread(Func&& func, Args&&... args)
        : std::thread{[=]{start(std::forward<Func>(func),
                std::forward<Args>(args)...);}} {
    }
};

g++ -std=c++11 has the following issue with the above code:

MyThread.h: In lambda function:
MyThread.h:ii:jj: error: parameter packs not expanded with '...':
                   std::forward<Args>(args)...);}}
                                      ^

I've tried a dozen different variations in the initializer-list to no avail.

How can I do what I want?

Upvotes: 0

Views: 158

Answers (2)

TrentP
TrentP

Reputation: 4692

The biggest difficulty I had when I did this before was getting all the behavior of std::thread. It's constructor can take not only a pointer to a free function, but also a class method pointer and then a object of the class as the first argument. There are a number of variations on that: class methods, class function object data members, an object of the class type vs a pointer to an object, etc.

This is for C++14:

class MyThread : public std::thread {
    void prolog() const { std::cout << "prolog\n"; }

 public:
    template <typename... ArgTypes>
    MyThread(ArgTypes&&... args) :
        std::thread(
            [this, bfunc = std::bind(std::forward<ArgTypes>(args)...)]
            () mutable {
                prolog();
                bfunc();
            })
    { }
};

If you put the prolog code inside the lambda and it doesn't call class methods, then the capture of this is not needed.

For C++11, a small change is needed because of the lack of capture initializers, so the bind must be passed as an argument to std::thread:

std::thread(
    [this]
    (decltype(std::bind(std::forward<ArgTypes>(args)...))&& bfunc) mutable {
        prolog();
        bfunc();
    }, std::bind(std::forward<ArgTypes>(args)...))

Here's a test program that also exercises the class member form of std::thread:

int main()
{
    auto x = MyThread([](){ std::cout << "lambda\n"; });
    x.join();

    struct mystruct {
        void func() { std::cout << "mystruct::func\n"; }
    } obj;
    auto y = MyThread(&mystruct::func, &obj);
    y.join();

    return 0;
}

I haven't checked, but I'm a bit worried that the capture of this, also seen in other solutions, is not safe in some cases. Consider when the object is an rvalue that is moved, as in std::thread t = MyThread(args). I think the MyThread object will go away before the thread it has created is necessarily finished using it. The "thread" will be moved into a new object and still be running, but the captured this pointer will point to a now stale object.

I think you need to insure your constructor does not return until your new thread is finished using all references or pointers to the class or class members. Capture by value, when possible, would help. Or perhaps prolog() could be a static class method.

Upvotes: 1

Richard Hodges
Richard Hodges

Reputation: 69892

This should do it (c++11 and c++14 solutions provided):

C++14

#include <thread>
#include <utility>
#include <tuple>

class MyThread : public std::thread {

    template<class Func, class ArgTuple, std::size_t...Is>
    void start(Func&& func, ArgTuple&& args, std::index_sequence<Is...>) {
        // Useful, thread-specific action
        func(std::get<Is>(std::forward<ArgTuple>(args))...);
    }
public:
    template<class Func, class... Args>
    MyThread(Func&& func, Args&&... args)
        : std::thread
        {
            [this,
            func = std::forward<Func>(func), 
            args = std::make_tuple(std::forward<Args>(args)...)] () mutable
            {
                using tuple_type = std::decay_t<decltype(args)>;
                constexpr auto size = std::tuple_size<tuple_type>::value;
                this->start(func, std::move(args), std::make_index_sequence<size>());
            }
        } 
    {
    }
};

int main()
{
    auto x = MyThread([]{});
}

In C++17 it's trivial:

#include <thread>
#include <utility>
#include <tuple>
#include <iostream>

class MyThread : public std::thread {

public:
    template<class Func, class... Args>
    MyThread(Func&& func, Args&&... args)
        : std::thread
        {
            [this,
            func = std::forward<Func>(func), 
            args = std::make_tuple(std::forward<Args>(args)...)] () mutable
            {
                std::cout << "execute prolog here" << std::endl;

                std::apply(func, std::move(args));

                std::cout << "execute epilogue here" << std::endl;
            }
        } 
    {
    }
};

int main()
{
    auto x = MyThread([](int i){
        std::cout << i << std::endl;
    }, 6);
    x.join();
}

C++11 (we have to facilitate moving objects into the mutable lambda, and provide the missing std::index_sequence):

#include <thread>
#include <utility>
#include <tuple>

namespace notstd
{
    using namespace std;

    template<class T, T... Ints> struct integer_sequence
    {};

    template<class S> struct next_integer_sequence;

    template<class T, T... Ints> struct next_integer_sequence<integer_sequence<T, Ints...>>
    {
        using type = integer_sequence<T, Ints..., sizeof...(Ints)>;
    };

    template<class T, T I, T N> struct make_int_seq_impl;

    template<class T, T N>
        using make_integer_sequence = typename make_int_seq_impl<T, 0, N>::type;

    template<class T, T I, T N> struct make_int_seq_impl
    {
        using type = typename next_integer_sequence<
            typename make_int_seq_impl<T, I+1, N>::type>::type;
    };

    template<class T, T N> struct make_int_seq_impl<T, N, N>
    {
        using type = integer_sequence<T>;
    };

    template<std::size_t... Ints>
        using index_sequence = integer_sequence<std::size_t, Ints...>;

    template<std::size_t N>
        using make_index_sequence = make_integer_sequence<std::size_t, N>;
}

template<class T>
struct mover
{
    mover(T const& value) : value_(value) {}
    mover(T&& value) : value_(std::move(value)) {}
    mover(const mover& other) : value_(std::move(other.value_)) {}

    T& get () & { return value_; }
    T&& get () && { return std::move(value_); }

    mutable T value_;
};

class MyThread : public std::thread {

    template<class Func, class ArgTuple, std::size_t...Is>
    void start(Func&& func, ArgTuple&& args, notstd::index_sequence<Is...>) {
        // Useful, thread-specific action
        func(std::get<Is>(std::forward<ArgTuple>(args))...);
    }
public:
    template<class Func, class... Args>
    MyThread(Func&& func, Args&&... args)
        : std::thread()
    {
        using func_type = typename std::decay<decltype(func)>::type;
        auto mfunc = mover<func_type>(std::forward<Func>(func));

        using arg_type = decltype(std::make_tuple(std::forward<Args>(args)...));
        auto margs = mover<arg_type>(std::make_tuple(std::forward<Args>(args)...));

        static_cast<std::thread&>(*this) = std::thread([this, mfunc, margs]() mutable
        {
                using tuple_type = typename std::remove_reference<decltype(margs.get())>::type;
                constexpr auto size = std::tuple_size<tuple_type>::value;
                this->start(mfunc.get(), std::move(margs).get(), notstd::make_index_sequence<size>());
        });

    }
};

int main()
{
    auto x = MyThread([](int i){}, 6);
    x.join();
}

Upvotes: 1

Related Questions