Search code examples
c++lambdastdthreadperfect-forwarding

Wrap std::thread in lambda with perfect forwarding


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;
    });
}

Solution

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