土法炼钢兴趣小组的算法知识备份

C++ 现代化封装

目录

Libevent 是纯 C 库,接口充满了 void* 和手动内存管理。在现代 C++ 项目中,我们希望拥有 RAII 的安全性和 Lambda 的便捷性。本篇将介绍如何优雅地封装 Libevent。

1. RAII 资源管理

使用 std::unique_ptr 自动管理 event_base, event, bufferevent 的生命周期,彻底告别内存泄露。

1.1. 自定义删除器 (Custom Deleter)

#include <memory>
#include <event2/event.h>
#include <event2/bufferevent.h>

// 定义删除器类型
struct EventBaseDeleter {
    void operator()(struct event_base* p) const { event_base_free(p); }
};
struct EventDeleter {
    void operator()(struct event* p) const { event_free(p); }
};
struct BuffereventDeleter {
    void operator()(struct bufferevent* p) const { bufferevent_free(p); }
};

// 定义智能指针别名
using EventBasePtr = std::unique_ptr<struct event_base, EventBaseDeleter>;
using EventPtr = std::unique_ptr<struct event, EventDeleter>;
using BuffereventPtr = std::unique_ptr<struct bufferevent, BuffereventDeleter>;

// 使用示例
void example() {
    EventBasePtr base(event_base_new());
    if (!base) throw std::runtime_error("Failed to create base");
    
    // base 离开作用域时自动释放
}

2. Lambda 回调封装

Libevent 的回调函数签名是固定的 C 函数指针: void (*)(evutil_socket_t, short, void *)

我们无法直接传入 C++ 的 Lambda(除非是无捕获的 Lambda)。解决方案是使用 Trampoline (蹦床) 模式。

2.1. 实现原理

  1. 将 Lambda 对象(std::function)的指针作为 void* arg 传入 Libevent。
  2. 编写一个静态的 C 函数作为“蹦床”。
  3. 在蹦床函数中,将 void* arg 强转回 std::function* 并调用。

2.2. 封装示例

#include <functional>
#include <iostream>

class Timer {
public:
    using Callback = std::function<void()>;

    Timer(struct event_base* base, Callback cb) 
        : cb_(std::move(cb)) {
        // 传入 this 指针作为参数
        ev_.reset(evtimer_new(base, &Timer::trampoline, this));
    }

    void start(int seconds) {
        struct timeval tv = {seconds, 0};
        evtimer_add(ev_.get(), &tv);
    }

private:
    // 蹦床函数
    static void trampoline(evutil_socket_t, short, void* arg) {
        Timer* self = static_cast<Timer*>(arg);
        if (self->cb_) {
            self->cb_();
        }
    }

    EventPtr ev_;
    Callback cb_;
};

// 使用
void test(struct event_base* base) {
    // 可以捕获上下文的 Lambda
    int count = 0;
    auto t = new Timer(base, [&count]() {
        std::cout << "Timeout! Count: " << ++count << std::endl;
    });
    t->start(1);
}

3. 进阶:避免悬垂指针

上面的例子有一个隐患:如果 Timer 对象被销毁了,但 Libevent 中还有挂起的事件,回调触发时访问 this 指针会导致 Crash。

3.1. 解决方案

  1. 在析构函数中取消事件: evtimer_del(ev_.get())。这是最安全的做法。
  2. 使用 std::shared_ptrstd::weak_ptr: 在回调中检查对象是否还活着。

4. 总结

通过 RAII 和 Trampoline 模式,我们可以将 Libevent 包装成符合 C++ 习惯的现代库。这不仅提高了代码的可读性,更大大降低了内存管理错误的风险。


上一篇: 04-architecture/process-pitfalls.md - 进程模型陷阱 下一篇: 04-architecture/business-integration.md - 业务集成模式

返回 Libevent 专题索引


By .