C++11 lambda表达式在回调函数中的使用方式

 更新时间:2022年11月05日 10:13:51   作者:MC-Zhang  
这篇文章主要介绍了C++11 lambda表达式在回调函数中的使用方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

在回调函数中使用lambda表达式的好处,在于可以利用C++的RAII机制来做资源的自动申请释放,避免手动管理出错。

一、lambda表达式在C++异步框架中的应用

1. 一个boost asio的例子

//
// async_tcp_echo_server.cpp
// ~~~~~~~~~~~~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2020 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//

#include <cstdlib>
#include <iostream>
#include <memory>
#include <utility>
#include <boost/asio.hpp>

using boost::asio::ip::tcp;

class session
  : public std::enable_shared_from_this<session>
{
public:
  session(tcp::socket socket)
    : socket_(std::move(socket))
  {
  }

  void start()
  {
    do_read();
  }

private:
  void do_read()
  {
    auto self(shared_from_this());
    socket_.async_read_some(boost::asio::buffer(data_, max_length),
        [this, self](boost::system::error_code ec, std::size_t length)
        {
          if (!ec)
          {
            do_write(length);
          }
        });
  }

  void do_write(std::size_t length)
  {
    auto self(shared_from_this());
    boost::asio::async_write(socket_, boost::asio::buffer(data_, length),
        [this, self](boost::system::error_code ec, std::size_t /*length*/)
        {
          if (!ec)
          {
            do_read();
          }
        });
  }

  tcp::socket socket_;
  enum { max_length = 1024 };
  char data_[max_length];
};

class server
{
public:
  server(boost::asio::io_context& io_context, short port)
    : acceptor_(io_context, tcp::endpoint(tcp::v4(), port))
  {
    do_accept();
  }

private:
  void do_accept()
  {
    acceptor_.async_accept(
        [this](boost::system::error_code ec, tcp::socket socket)
        {
          if (!ec)
          {
            std::make_shared<session>(std::move(socket))->start();
          }

          do_accept();
        });
  }

  tcp::acceptor acceptor_;
};

int test()
{
  try
  {

    boost::asio::io_context io_context;

    server s(io_context, 8080);

    io_context.run();
  }
  catch (std::exception& e)
  {
    std::cerr << "Exception: " << e.what() << "\n";
  }

  return 0;
}

int main(int argc, char** argv){
  test();
}

其中在async_read_some函数的第二个参数使用了lambda表达式作为参数,并且闭包捕获了self变量。这样做的目的是通过shared_ptr增加this的引用计数。 在server::do_accept函数中存在一句代码:

std::make_shared(std::move(socket))->start();

这里有一个表达式亡值,属于shared_ptr类型。当启动start()方法后,会为该智能指针所管理的对象增加一次引用计数。 所以在离开作用域后,shared_ptr析构不会导致实际的session对象析构。

最终当不再继续注册异步读写回调时(在这里的代码中,当读写出现错误时),即放弃该连接的session时, 智能指针的引用计数降为0,触发session对象的析构。

void do_read()
{
    auto self(shared_from_this());
    socket_.async_read_some(boost::asio::buffer(data_, max_length),
        [this, self](boost::system::error_code ec, std::size_t length)
        {
          if (!ec)
          {
            do_write(length);
          }
        });
}
void do_accept()
{
    acceptor_.async_accept(
        [this](boost::system::error_code ec, tcp::socket socket)
        {
          if (!ec)
          {
            std::make_shared<session>(std::move(socket))->start();
          }

          do_accept();
        });
}

这样使用lambda表达式在资源管理上带来了传统的函数指针不具备的优势。因为当回调函数被执行时,使用传统写法需要在每个条件分支下都要考虑到资源的释放。

2. C++ http框架cutelyst在异步执行PostgreSQL数据库sql请求的例子

void SingleDatabaseQueryTest::dbp(Context *c)
{
    const int id = (qrand() % 10000) + 1;

    ASync async(c);
    static thread_local auto db = APool::database();
    db.exec(APreparedQueryLiteral(u"SELECT id, randomNumber FROM world WHERE id=$1"),
                           {id}, [c, async] (AResult &result) {
        if (Q_LIKELY(!result.error() && result.size())) {
            auto it = result.begin();
            c->response()->setJsonBody(QByteArray::fromStdString(
                            picojson::value(picojson::object({
                                                {"id", picojson::value(double(it[0].toInt()))},
                                                {"randomNumber", picojson::value(double(it[1].toInt()))}
                                            })).serialize()));
            return;
        }

        c->res()->setStatus(Response::InternalServerError);
    }, c);
}

其中ASync的构造函数作用是断开事件处理链,即当这个dbp函数返回时,对该context不去向浏览器发出http响应。代码大致为:

ASync::ASync(Context *c)
{
    c->detachAsync();
}

析构函数作用是恢复事件处理链,即通知eventloop,可以对该context发送http响应了。大致为:

ASync::~ASync()
{
    c->attachAsync();
}

通过在异步sql执行函数中注册一个lambda表达式,lambda表达式捕获一个外部变量,利用RAII机制,能够实现在异步sql执行完毕后再进行http响应。这是lambda表达式闭包捕获变量的优势。

二、如何在C-style注册回调函数中使用lambda表达式?

有一个c库,其中存在一个注册回调函数:

void register_callback(void(*callback)(void *), void * context);

希望可以注册C++11的lambda表达式,而且是带捕获变量的lambda表达式,因为要用捕获变量来维持状态。

首先分析一下这个注册函数:

这个注册回调函数第一个参数是C-style函数指针,不带状态。第二个参数void *context ,携带函数执行的状态。

这样每次函数执行的时候,会将context传递进来,做到了持续保持对状态的访问和修改。

void register_callback( void(*callback)(void*), void * p ) {
  //这里是一个简单的模拟。实际中可能会多次调用callback函数。
  callback(p); //  测试
  callback(p);
}

对于将lambda表达式与函数指针之间的转换,如果没有捕获变量可以直接转换。

void raw_function_pointer_test() {
  int x = 0;
  auto f = [](void* context)->void {
      int *x = static_cast<int*>(context);
      ++(*x); 
  };
  register_callback(f, &x);
  std::cout << x << "\n";
}

调用代码

raw_function_pointer_test();

输出:2

但是这种转换方式,完全属于C风格,充满了类型不安全。如果想要使用lambda表达式来直接捕获变量x,则不行。下面这个代码无法通过编译。

void raw_function_pointer_capture_test() {
  int x = 0;
  auto f = [x](void* context) mutable ->void {
      ++x; 
  };
  register_callback(f, nullptr);
  std::cout << x << "\n";
}

那有什么方法能够将捕获变量的lambda表达式转换成普通函数指针,同时能够保留状态呢?

方法一: 声明一个全局的invoke_function函数,将lambda表达式转为为void*,即将lambda表达式作为状态传递。

extern "C" void invoke_function(void* ptr) {
    (*static_cast<std::function<void()>*>(ptr))();
}
void lambda_to_function(){
    int x = 0;
    auto lambda_f = [&]()->void { ++x; };
    std::function cpp_function(std::move(lambda_f));
    register_callback(&invoke_function, &cpp_function);
    std::cout << x << "\n";
}

调用代码

lambda_to_function();

输出:2

std::function cpp_function用于接管lambda表达式的所有权,状态都存在这里。此处使用的是栈变量,可以根据实际的需要变成堆变量,防止cpp_function析构后再使用,成为undefined behavior。

方法二:使用模板,将状态存在一个结构体里面。

#include <iostream>
#include <tuple>
#include <memory>

template<class...Args>
struct callback {
  void(*function)(void*, Args...)=nullptr;
  std::unique_ptr<void, void(*)(void*)> state;
};
template<typename... Args, typename Lambda>
callback<Args...> voidify( Lambda&& l ) {
  using Func = typename std::decay<Lambda>::type;
  std::unique_ptr<void, void(*)(void*)> data(
    new Func(std::forward<Lambda>(l)),
    +[](void* ptr){ delete (Func*)ptr; }
  );
  return {
    +[](void* v, Args... args)->void {
      Func* f = static_cast< Func* >(v);
      (*f)(std::forward<Args>(args)...);
    },
    std::move(data)
  };
}


void lambda_capture_template_test() {
  int x = 0;
  auto closure = [&]()->void { ++x; };
  auto voidified = voidify(closure);
  register_callback( voidified.function, voidified.state.get() );
//   register_callback( voidified.function, voidified.state.get() );
  std::cout << x << "\n";
}

调用代码

lambda_capture_template_test();

输出:2

稍微解释一下模板做法的含义。

template<class...Args>
struct callback {
  void(*function)(void*, Args...)=nullptr;
  std::unique_ptr<void, void(*)(void*)> state;
};

这个模板类callback,第一个成员就是普通函数指针,用于注册回调函数时使用。第二个成员是自定义deleter的unique_ptr,智能指针管理的是一个匿名类(即lambda表达式所属的类)。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • MATLAB全网最全的colormap的使用教程详解

    MATLAB全网最全的colormap的使用教程详解

    众所周知,MATLAB中的colormap只有少得可怜的几种,有很多应用在很特殊的图形中的colormap几乎都没有,而每次写代码都要去找颜色的图属实太麻烦。所以本文将包全部集成了进来,终于有了这套包含200个colormap的工具函数,希望对大家有所帮助
    2023-02-02
  • C语言关于注释的知识点总结

    C语言关于注释的知识点总结

    在本篇文章里小编给大家分享的是关于C语言关于注释的知识点总结,需要的朋友们可以参考学习下。
    2020-02-02
  • Qt5多线程编程的实现

    Qt5多线程编程的实现

    Qt通过三种形式提供了对线程的支持,本文主要介绍了Qt5多线程编程的实现,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-12-12
  • C语言巧用二分查找实现猜数游戏

    C语言巧用二分查找实现猜数游戏

    二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列,本篇文章教你用二分查找编写猜数字游戏
    2022-02-02
  • Qt禁止程序多开的实现示例

    Qt禁止程序多开的实现示例

    本文主要介绍了Qt 禁止程序多开的实现示例,主要介绍了三种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-09-09
  • C++静态持续变量介绍

    C++静态持续变量介绍

    这篇文章主要介绍了 C++静态持续变量,静态持续变量的定义C++和C语言是一样的,它拥有三种链接性,即外部链接性、内部连接性和无链接性。其中外部链接性指的是可以在其他文件中访问,内部链接性指的是只能在当前文件访问,需要的朋友可以参考一下
    2021-11-11
  • C++指针学习详解

    C++指针学习详解

    指针在 C\C++ 语言中是很重要的内容,并且和指针有关的内容一向令初学者头大,这篇文章主要给大家介绍了关于C/C++中指针的相关资料,需要的朋友可以参考下
    2021-09-09
  • C语言模拟掷骰子游戏

    C语言模拟掷骰子游戏

    这篇文章介绍了C语言模拟掷骰子游戏的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。需要的朋友可以收藏下
    2021-11-11
  • 一篇文章带你了解C++的KMP算法

    一篇文章带你了解C++的KMP算法

    这篇文章主要介绍了c++ 实现KMP算法的示例,帮助大家更好的理解和学习c++,感兴趣的朋友可以了解下,希望能给你带来帮助
    2021-08-08
  • C#将Unicode编码转换为汉字字符串的简单方法

    C#将Unicode编码转换为汉字字符串的简单方法

    下面小编就为大家带来一篇C#将Unicode编码转换为汉字字符串的简单方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-01-01

最新评论