在回调函数中使用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 )
{
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表达式所属的类)。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程网。