0

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?

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
Steve Emmerson
  • 7,702
  • 5
  • 33
  • 59
  • Capturing everything by value in the lambda is not what you want to do. Remove the `=` and let the compiler tell you what you need to capture and think about how you want to capture those. – nwp May 16 '17 at 21:19
  • @nwp Doesn't seem to make any difference: still won't compile. – Steve Emmerson May 16 '17 at 21:40
  • The `...` before your comment is not valid `C++` – Galik May 16 '17 at 21:51
  • This is a bit tricky in C++11 due to the lack of [capturing expressions](https://stackoverflow.com/questions/25408190/lambda-captures-c14), so it is not possible to just forward-capture the arguments into the lambda. I'll give it a try tomorrow if no-one else answers. Alternatively you could lift the C++11 requirement to get a better solution faster if that is an option. – nwp May 16 '17 at 21:52
  • There's a fundamental problem here: if you can get it to compile, the thread will start running, and potentially call `MyThread::start`, before the constructor of `MyThread` has finished executing. This is a data race, and the behavior is undefined. This is not how `std::thread` was designed to be used. – Pete Becker May 16 '17 at 22:16
  • @Galik True but irrelevant – Steve Emmerson May 18 '17 at 19:33
  • @PeteBecker I don't see any problem. As written, there's no race condition: only `func` and `args` are accessed on the new thread and the constructor doesn't modify them. Additionally, any race condition should be amenable to the standard solutions (atomic variables, mutexes, etc.). – Steve Emmerson May 18 '17 at 19:40

2 Answers2

1

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();
}
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • Seriously?! There's no simpler C++11 solution? – Steve Emmerson May 18 '17 at 19:45
  • @SteveEmmerson most of the complication here is in polyfilling the missing std::index_sequence. – Richard Hodges May 18 '17 at 20:02
  • I don't doubt it. I'm just dismayed that the solution is so complicated. Couldn't something be done with `std::bind()`? GNU seems to use that mechanism -- though the details are a bit beyond my current understanding of the language. – Steve Emmerson May 19 '17 at 16:32
  • @SteveEmmerson `std::bind` was an anachronism as soon as it went into the standard. It does the job of a lambda, but less well. It was necessary in boost in c++03 in order to get lambda-like behaviour. c++17 has std::apply, which would make this solution a 3-liner. What I have essentially done is provide a cut-down std::apply tailored for the job. – Richard Hodges May 19 '17 at 16:35
  • @SteveEmmerson added c++17 solution to the answer – Richard Hodges May 19 '17 at 16:41
  • These only work using a pointer to a function as the `Func` argument. `std::thread` runs the supplied arguments as if using `std::invoke` (c++17), which allows a number of other possibilities, based around supplying a pointer to a class method or data member and an object or pointer to on object of the class. This doesn't here because `func(args...)` needs to be something like `arg1.->func(args2plus...)`. – TrentP May 20 '17 at 06:11
  • @TrentP a function pointer or a function object, yes. I am aware of this, but I felt that to re-implement invoke in c++11 was well beyond the scope of the question. I have assumed in the OP's code that `F` was 'some function-like object'. This could be created with std::mem_fn, `std::bind`, `std::function`, lambda et.al. – Richard Hodges May 20 '17 at 06:34
  • @RichardHodges I tried that, implementing `std::invoke()` in C++14, and decided it was too much. Though about 75% of the possibilities aren't that hard and less code than the make_index stuff. Then I realized `std::invoke(args...)` can be emulated with `std::bind(args...)()`, and also that `std::thread(args..)` is more or less internally `thread(args...) : m_func(std::bind(args...)) { start_new_thread(m_func); }` and so I could use bind to get the features of std::thread. – TrentP May 20 '17 at 07:20
1

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.

TrentP
  • 4,240
  • 24
  • 35
  • I understand your concern about capturing `this`. To be honest, the standard c++ way is to treat the thread as an implementation detail (i.e. make it a member variable of an impl, rather than derive from it). The MFC way is was to derive from the thread object, but this turns out to be problematic (as you are no doubt experiencing). – Richard Hodges May 20 '17 at 09:23