【问题标题】:Passing a lambda with moved capture to function将带有移动捕获的 lambda 传递给函数
【发布时间】:2018-09-15 13:09:26
【问题描述】:

我最近遇到了一个对我来说很难找到的错误。我试图将 lambda 传递给采用 std::function 对象的函数。 lambda 正在捕获一个不可复制的对象。

我想通了,显然在所有传球之间一定会发生一些副本。我得出这个结果是因为我总是以 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. 我的想法:我在for 循环中生成了许多fstream 对象和lambda,并且对于每个fstream,都有一个lambda 写入它。所以对fstream 对象的访问只能由lambdas 完成。我想为一些回调逻辑这样做。有没有像我尝试过的那样使用 lambdas 更漂亮的方法?

【问题讨论】:

  • /OT:不要使用const_cast,而是将你的lambda标记为mutable
  • 顺便说一句,您的 const_cast 是 UB。您不能修改以这种方式投射的对象。正如 Rakete1111 指出的那样,正确的方法是标记对象mutable
  • 为什么我必须让它可变?
  • @CássioRenan 这里的 UB 是什么?成员变量被捕获为非const;只有生成的operator() 被标记为const。将constconst 左值转换为非const 对象以修改所述对象是完全没问题的——而且const_cast 的全部意义。当然,这里的风格不好,我不认为,但它不是 UB。或者如果是,为什么?

标签: c++ lambda c++14


【解决方案1】:

发生错误是因为您的 lambda 具有不可复制的捕获,使 lambda 本身不可复制。 std::functionrequires that the wrapped object be copy-constructible.

如果您可以控制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。我更喜欢后者。

【讨论】:

  • 如果知道mutable lambda 只会运行一次,它就没有问题。例如,将可变 lambda 推送到工作队列没有风险。
  • 首先,非常感谢,但是 auto lam = [fs = std::move(fs)] () mutable { fs.close(); };调用函数(拉姆);对我来说确实以同样的错误结束。并且您的其他解决方案不适合,因为当调用 lambda 时,std::fstream 对象长期超出范围。
  • @JulianH 对不起,我不明白你的问题。您可以将可复制对象移动到 lambda 中,并且可以正常使用 std::function。例如:仅仅因为您正在移动对象,这并不意味着该对象必须不可复制。我在 (2) 中根据您的想法编辑了我的答案
  • @underscore_d 你是对的。编辑答案以删除 OP 代码中不必要的更改。
  • @underscore_d 老实说,我不认为这两者中的任何一个都是好的风格。但这只是我。有太多方法可以通过细微的更改来引入无声的错误。我在回复上面弗朗索瓦的评论时引用了其中一种方式。此外,const_cast 还存在一个额外的问题,即如果您确实将 lambda 传递给 const 对象,它将导致 UB 而不是导致编译器错误(就像mutable lambda 那样)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-13
  • 1970-01-01
  • 2017-03-07
相关资源
最近更新 更多