Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Passing a lambda with moved capture to function

Tags:

c++

lambda

c++14

I recently struggled with a bug hard to find for me. I tried to pass a lambda to a function taking a std::function object. The lambda was capturing a noncopyable object.

I figured out, obviously some copy must happen in between all the passings. I came to this result because I always ended in an error: use of deleted function error.

Here is the code which produces this error:

void call_func(std::function<void()> func)
{
    func();
}

int main()
{
    std::fstream fs{"test.txt", std::fstream::out};
    auto lam = [fs = std::move(fs)] { const_cast<std::fstream&>(fs).close(); };
    call_func(lam);
    return 0;
}

I solved this by capseling the std::fstream object in an std::shared_ptr object. This is working fine, but I think there may be a more sexy way to do this.

I have two questions now:

  1. Why is this error raising up?
  2. My idea: I generate many fstream objects and lambdas in a for loop, and for each fstream there is one lambda writing to it. So the access to the fstream objects is only done by the lambdas. I want do this for some callback logic. Is there a more pretty way to this with lambdas like I tried?
like image 425
JulianH Avatar asked Sep 15 '18 13:09

JulianH


1 Answers

The error happens because your lambda has non-copyable captures, making the lambda itself not copyable. std::function requires that the wrapped object be copy-constructible.

If you have control over call_func, make it a template:

template<typename T>
void call_func(T&& func)
{
    func();
}

int main()
{
    std::fstream fs{"test.txt", std::fstream::out};
    auto lam = [fs = std::move(fs)] { const_cast<std::fstream&>(fs).close(); };
    call_func(lam);
}

Following is my take on your idea in (2). Since std::function requires the wrapped object to be copy-constructible, we can make our own function wrapper that does not have this restriction:

#include <algorithm>
#include <fstream>
#include <iterator>
#include <utility>
#include <memory>
#include <sstream>
#include <vector>

template<typename T>
void call_func(T&& func) {
    func();
}

// All functors have a common base, so we will be able to store them in a single container.
struct baseFunctor {
    virtual void operator()()=0;
};

// The actual functor is as simple as it gets.
template<typename T>
class functor : public baseFunctor {
    T f;
public:
    template<typename U>
    functor(U&& f)
        :    f(std::forward<U>(f))
    {}
    void operator()() override {
        f();
    }
};

// In C++17 you don't need this: functor's default constructor can already infer T.
template<typename T>
auto makeNewFunctor(T&& v) {
    return std::unique_ptr<baseFunctor>(new functor<T>{std::forward<T>(v)});
}

int main() {
    // We need to store pointers instead of values, for the virtual function mechanism to behave correctly.
    std::vector<std::unique_ptr<baseFunctor>> functors;

    // Generate 10 functors writing to 10 different file streams
    std::generate_n(std::back_inserter(functors), 10, [](){
        static int i=0;
        std::ostringstream oss{"test"};
        oss << ++i << ".txt";
        std::fstream fs{oss.str(), std::fstream::out};
        return makeNewFunctor([fs = std::move(fs)] () mutable { fs.close(); });
    });

    // Execute the functors
    for (auto& functor : functors) {
        call_func(*functor);
    }
}

Note that the overhead from the virtual call is unavoidable: Since you need functors with different behavior stored in the same container, you essentially need polymorphic behavior one way or the other. So you either implement this polymorphism by hand, or use virtual. I prefer the latter.

like image 164
Cássio Renan Avatar answered Oct 16 '22 02:10

Cássio Renan