C++11 lambda表达式在回调函数中的使用方式
作者:MC-Zhang
在回调函数中使用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表达式所属的类)。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。