如何编写一个匿名函数/lambda表达式,使其作为回调函数传递自身?

8

我正在同时学习boost::asio和C++11。我的一个测试程序实际上是适应于 boost::asio教程中给出的示例之一,代码如下:

#include <iostream>
#include <boost/asio.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>

class printer {

// Static data members
private:
    const static boost::posix_time::seconds one_second;

// Instance data members
private:
    boost::asio::deadline_timer timer;
    int count;

// Public members
public:
    printer(boost::asio::io_service& io)
        : timer(io, one_second), count(0) {

        std::function<void(const boost::system::error_code&)> callback;
        callback = [&](const boost::system::error_code&) { // critical line
            if (count < 5) {
                std::cout << "Current count is " << count++ << std::endl;

                timer.expires_at(timer.expires_at() + one_second);
                timer.async_wait(callback);
            }
        };

        timer.async_wait(callback);
    }

    ~printer() {
        std::cout << "Final count is " << count << std::endl;
    }
};

const boost::posix_time::seconds printer::one_second(1);

int main() {
    boost::asio::io_service io;
    printer p(io);
    io.run();

    return 0;
}

当我运行这个程序时,出现了分段错误。我知道为什么会出现分段错误。在构造函数运行完毕后,构造函数的callback变量超出了作用域,lambda的callback变量成为了悬空引用,它是对构造函数的callback变量的引用。
因此,我修改了关键行:
        callback = [callback, &](const boost::system::error_code&) { // critical line

然后编译它,运行它,却得到了一个坏的函数调用错误。我理解为什么会出现这个错误。在lambda的作用域内,构造函数的callback变量仍然没有被赋值,因此实际上它是一个悬空函数指针。因此,lambda的callback变量(构造函数的callback变量的副本)也是一个悬空的函数指针。
思考了一段时间后,我意识到我真正需要的是回调能够使用函数指针引用自身,而不是引用函数指针的引用。该示例通过使用命名函数作为回调来实现了这一点,而不是使用匿名函数。但是,将命名函数作为回调传递并不是非常优雅。有没有办法让匿名函数拥有一个指向自身的函数指针作为局部变量?
5个回答

7
有几种替代方案:
  • Stop using a lambda. You don't have to use them for everything, you know. They cover a lot of cases, but they don't cover everything. Just use a regular old functor.
  • Have the lambda store a smart pointer to a dynamically allocated std::function that stores the lambda. For example:

    auto pCallback = std::make_shared<std::function<void(const boost::system::error_code&)>>();
    auto callback = [=](const boost::system::error_code&) { // critical line
        if (count < 5) {
            std::cout << "Current count is " << count++ << std::endl;
    
            timer.expires_at(timer.expires_at() + one_second);
            timer.async_wait(pCallback.get());
        }
    };
    *pCallback = callback;
    

使用函数对象与使用命名函数完全相同。函数对象的名称本质上就是函数的真实名称。此外,如果我无论如何都要使用函数对象,那为什么不将“printer”类本身作为函数对象呢? - isekaijin
1
@EduardoLeón:就像我说的,Lambda并不适用于所有情况。有时候,你需要接受你所面临的限制,并在其中工作。另外,你也可以选择我之前提到的其他方法。就我个人而言,如果你必须使用指针方法来完成某项任务,这就强烈暗示你应该使用更为清晰和明显的functor方法。至少,通过该方法,你所要做的事情是显而易见的。 - Nicol Bolas

4

如果想了解Asio和C++11,我建议观看boostcon演讲《为什么C++0x是最棒的网络编程语言》,演讲者是asio的设计者Christopher Kohlhoff。

https://blip.tv/boostcon/why-c-0x-is-the-awesomest-language-for-network-programming-5368225 http://github.com/chriskohlhoff/awesome

在这次演讲中,C.K拿了一个典型的小asio应用程序,并逐步添加C++11功能。演讲中间有一个关于lambda的部分。使用以下模式可以解决与lambda生命周期相关的问题:
#include <iostream>
#include <boost/asio.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <memory>

class printer 
{

// Static data members
private:
    const static boost::posix_time::seconds one_second;

// Instance data members
private:
    boost::asio::deadline_timer timer;
    int count;

// Public members
public:
    printer(boost::asio::io_service& io)
        : timer(io, one_second), count(0) {
       wait();
    }

    void wait() {
        timer.async_wait(
            [&](const boost::system::error_code& ec) {
               if (!ec && count < 5) {
                 std::cout << "Current count is " << count++ << std::endl;

                 timer.expires_at(timer.expires_at() + one_second);
                 wait();
               }
            });
    }

    ~printer() {
        std::cout << "Final count is " << count << std::endl;
    }
};

const boost::posix_time::seconds printer::one_second(1);

int main() {
    boost::asio::io_service io;
    printer p(io);
    io.run();

    return 0;
}

它在技术上并没有回答这个问题,但确实解决了问题。+1。 - Nicol Bolas

2
现在有一个提案要将Y组合子添加到C ++标准库中(P0200R0),以解决这个问题。基本思想是将lambda作为第一个参数传递给自己。一个不可见的辅助类通过将lambda存储在命名成员中来处理后台的递归调用。提案中的示例实现如下:
#include <functional>
#include <utility>

namespace std {

template<class Fun>
class y_combinator_result {
    Fun fun_;
public:
    template<class T>
    explicit y_combinator_result(T &&fun): fun_(std::forward<T>(fun)) {}

    template<class ...Args>
    decltype(auto) operator()(Args &&...args) {
        return fun_(std::ref(*this), std::forward<Args>(args)...);
    }
};

template<class Fun>
decltype(auto) y_combinator(Fun &&fun) {
    return y_combinator_result<std::decay_t<Fun>>(std::forward<Fun>(fun));
}

} // namespace std

以下是可用于解决问题的方法:
可以按照以下方式使用:
timer.async_wait(std::y_combinator([](auto self, const boost::system::error_code&) {
    if (count < 5) {
        std::cout << "Current count is " << count++ << std::endl;

        timer.expires_at(timer.expires_at() + one_second);
        timer.async_wait(self);
    }
}));

请注意被传递到lambda的self参数。这将绑定到y_combinator调用的结果,它是一个函数对象,相当于已经绑定了self参数的lambda(即其签名为void(const boost::system::error_code&))。

有用,但真的很丑陋。手动绑定固定点不应该是一件事。 - isekaijin

1

这个 lambda 表达式通过引用捕获了本地变量“callback”,当 lambda 运行时,该变量将不再有效。


1
我非常明确地表达了我理解为什么我的程序不起作用。我想知道一个lambda如何能够有一个指向自己的函数指针(永久存在),而不是一个指向自身函数指针的引用(一旦实际函数指针变量超出范围,可能会成为悬挂引用)。 - isekaijin
好的,当将其简化为为什么它不起作用的原则时,我认为解决方案有些清晰。您尝试过将“回调”作为类的成员吗? - Adam Mitz

1

只是理论:你可以使用所谓的组合子(如 I、S、K)来做这种事情。

在使用类型为 F 的匿名 lambda 表达式 e 之前,您可以先定义函数,例如 doubleF;(F) -> (F, F),或者 applyToOneself:(F f) -> F = { return f(f); }。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接