4

I need to wrap std::thread to do some processing (inside the new thread) before the user function runs.

At the moment I'm achieving this via a helper function, but that's a bit annoying.

How can I convert wrapThreadHelper into a lambda inside wrapThread while keeping the perfect forwarding semantics?

#include <functional>
#include <iostream>
#include <string>
#include <thread>

using namespace std;

struct Foo
{
    void bar()
    {
        cout << "bar is running" << endl;
    }
};

template <class F, class... Args>
void wrapThreadHelper(F &&f, Args &&...args)
{
    cout << "preparing the thread..." << endl;
    std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
    cout << "cleaning the thread..." << endl;
}

template <class F, class... Args>
std::thread wrapThread(F &&f, Args &&...args)
{
    return std::thread(&wrapThreadHelper<F, Args...>, std::forward<F>(f), std::forward<Args>(args)...);
}

int main()
{
    Foo foo;
    std::thread t1 = wrapThread(&Foo::bar, std::ref(foo));

    std::thread t2 = wrapThread([] { cout << "lambda is running..."; });

    t1.join();
    t2.join();

    return 0;
}

I would like to delete wrapThreadHelper and convert wrapThread into something like this:

template <class F, class... Args>
std::thread wrapThread(F &&f, Args &&...args)
{
    return std::thread([]() {
        cout << "preparing the thread..." << endl;
        std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
        cout << "cleaning the thread..." << endl;
    });
}
1

2 Answers 2

8

Naive solution

The easiest and shortest way is to capture the function parameters:

template <class F, class... Args>
std::thread wrapThread(F &&f, Args &&...args)
{
    return std::thread([=]() mutable {
        cout << "preparing the thread..." << endl;
        std::invoke(std::move(f), std::move(args)...);
        cout << "cleaning the thread..." << endl;
    });
}

Note that we need to capture by copy = because by the time the thread invokes the lambda we give it, the function parameters may no longer exist. This is problematic when the parameters are temporary objects like std::ref(foo).

Since the lambda stores unique copies of f and args, it should also use std::move (not std::forward) in the lambda body to transfer ownership of its captured copies to std::invoke.

Improved solution

Unfortunately, [=] would result in unnecessary copying, but we can fix this with generalized captures:

template <class F, class... Args>
std::thread wrapThread(F &&f, Args &&...args)
{
    return std::thread([f = std::forward<F>(f), ...args = std::forward<Args>(args)]()
    mutable {
        cout << "preparing the thread..." << endl;
        std::invoke(std::move(f), std::move(args)...);
        cout << "cleaning the thread..." << endl;
    });
}

Alternative solution

Yet another solution can be seen at https://stackoverflow.com/a/34731847/5740428, which would involve letting the std::thread store the function parameters like std::forward<F>(f) and giving the lambda corresponding rvalue reference parameters like std::decay_t<F>&&:

template <class F, class... Args>
std::thread wrapThread(F &&f, Args &&...args)
{
    return std::thread([](std::decay_t<F> &&f, std::decay_t<Args> &&...args) {
        cout << "preparing the thread..." << endl;
        std::invoke(std::move(f), std::move(args)...);
        cout << "cleaning the thread..." << endl;
    }, std::forward<F>(f), std::forward<Args>(args)...);
}

While this solution is more complicated, it's technically optimal. Using a capturing lambda results in at least two calls to move constructors (one when creating the lambda, one when moving the lambda into the std::thread), while this solution avoids one of these calls.

It's up to you whether you actually care or simply assume that move constructors are cheap, in which case the previous solution is best.

Sign up to request clarification or add additional context in comments.

6 Comments

In the alternative solution, why do you decay_t the parameters in the forwarding lambda? std::thread has already decayed them. At this point, it's just a matter of forwarding them perfectly again to std::invoke.
Let's say you call wrapThread(f, "abc"). "abc" is of type const char[4] = Args and std::thread will store a decay-copy, i.e. const char*. It is obviously wrong for the lambda to have an Args&&... = const char(&)[4] parameter pack then; the parameter type needs to match the decay that the std::thread already does. It would also be possible to ignore the whole problem by using a generic lambda with auto&& parameters, but that would unnecessarily make it a template, and that tends to give you worse errors, debugging experience, etc.
Yes, I was expecting a parameter pack around auto&&, because the topic is about perfect forwarding, and not how to avoid it.
@JanSchultke Thanks! Could you please explain a couple of things? A) Why pass the args of std::invoke via std::move instead of std::forward? B) Why does the code only work when calling wrapThread with std::ref? If I pass an lvalue it doesn't compile.
It's using std::move because its moving its own copies to std::move; it's not forwarding references or something. The lambda contains distinct objects. This also applies to the alternative solution, where std::thread stores copies and will std::move them when calling the lambda, and then you forward by moving. I cannot reproduce your issue with passing lvalues. Can you show an example based on godbolt.org/z/b69bqqbej?
|
1

In the following code the lambda captures the function and its arguments by value, ensuring that they are correctly forwarded. The use of mutable allows modification of captured variables if necessary. This approach effectively removes the need for the wrapThreadHelper function while maintaining the intended behavior

template <class F, class... Args>
std::thread wrapThread(F &&f, Args &&...args)
{
    return std::thread([f = std::forward<F>(f), ...args = std::forward<Args>(args)]() mutable {
        cout << "preparing the thread..." << endl;
        std::invoke(f, std::forward<Args>(args)...);
        cout << "cleaning the thread..." << endl;
    });
}

int main()
{
    Foo foo;
    std::thread t1 = wrapThread(&Foo::bar, std::ref(foo));
    
    std::thread t2 = wrapThread([] { cout << "lambda is running..."; });

    t1.join();
    t2.join();

    return 0;
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.