Передача лямбда с перемещенным захватом, чтобы функционировать - PullRequest
0 голосов
/ 15 сентября 2018

Я недавно боролся с ошибкой, которую трудно найти для меня. Я пытался передать лямбду в функцию, принимающую объект std::function. Лямбда захватывает некопируемый объект.

Я понял, очевидно, что какая-то копия должна произойти между всеми проходами. Я пришел к этому результату, потому что всегда заканчивался ошибкой error: use of deleted function.

Вот код, который выдает эту ошибку:

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

Я решил это, объединив объект std::fstream в объект std::shared_ptr. Это работает нормально, но я думаю, что может быть более сексуальный способ сделать это.

У меня сейчас два вопроса:

  1. Почему возникает эта ошибка?
  2. Моя идея: я генерирую много fstream объектов и лямбд в цикле for, и для каждого fstream есть одна лямбда-запись в него. Таким образом, доступ к fstream объектам осуществляется только через лямбды. Я хочу сделать это для некоторой логики обратного вызова. Есть ли более симпатичный способ для этого с лямбдами, как я пытался?

1 Ответ

0 голосов
/ 15 сентября 2018

Ошибка возникает из-за того, что ваша лямбда имеет не копируемые захваты, что делает саму лямбду не копируемой. std::function требует, чтобы обернутый объект был копируемым .

Если у вас есть контроль над call_func, сделайте его шаблоном:

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

Вот мое мнение о вашей идее в (2). Поскольку std::function требует, чтобы обернутый объект был конструируемым для копирования, мы можем создать нашу собственную функцию-обертку, которая не имеет этого ограничения:

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

Обратите внимание, что накладные расходы от виртуального вызова неизбежны: поскольку вам нужны функторы с различным поведением, хранящиеся в одном и том же контейнере, вам, по сути, нужно полиморфное поведение, так или иначе. Таким образом, вы либо реализуете этот полиморфизм вручную, либо используете virtual. Я предпочитаю последнее.

...