如何实现一个“重入安全”的 function_safe


在 C++ 网络编程或异步框架中,回调函数(Callback)无处不在。std::function 是我们最常用的工具,但它有一个致命的弱点:在回调函数执行的过程中,如果尝试修改这个回调函数本身,会发生什么?

通常情况下,这会导致未定义行为(Undefined Behavior)甚至程序崩溃。为了解决这个问题,ZLToolKit 提供了一个精妙的模板类 —— function_safe

本文将深入剖析它的设计思路、延迟更新机制以及其中涉及的 C++ 模板元编程技巧(SFINAE)。

1. 为什么我们需要 function_safe?

1.1. 痛点场景

假设我们正在编写一个网络服务器,处理 TCP 连接。

  1. 连接建立初期,我们需要处理握手协议(Handshake)。
  2. 握手完成后,我们需要将回调切换为处理业务数据(Data)。

代码可能长这样:

std::function<void(Data)> onRecv;

// 初始回调:处理握手
onRecv = [&](Data d) {
    if (isHandshake(d)) {
        // 握手成功,切换回调为处理业务数据
        // 危险!正在执行 onRecv 时修改 onRecv 本身!
        onRecv = [](Data d) { handleBusiness(d); }; 
    }
};

onRecv 正在执行内部逻辑时,其内部状态是被锁定的或处于特定栈帧中。此时直接对 onRecv 赋值,相当于在引擎运转时更换引擎,极易引发 Crash。

解决方案

function_safe 的核心目标就是:允许在回调执行过程中安全地更新自身

2. 核心机制:延迟更新 (Deferred Update)

function_safe 采用了一种类似“双缓存”的策略。它引入了两个 std::function 对象和两个状态标志位。

2. 1. 核心成员变量

private:
    mutable bool _update = false; // 标记是否有待处理的更新
    mutable bool _doing = false;  // 标记当前是否正在执行回调
    mutable func _tmp;            // 暂存区:用于存放新的回调
    mutable func _impl;           // 执行区:当前实际使用的回调

2.2. 执行流程 (operator())

当函数被调用时,利用 RAII(通过 onceToken)来管理状态:

  1. 进入前:设置 _doing = true,表示开始执行。
  2. 执行中:调用 _impl(...) 执行实际逻辑。
  3. 退出后:设置 _doing = false,并检查是否有挂起的更新(checkUpdate)。
R operator()(ArgTypes... args) const {
    // RAII 惯用法:构造时执行第一个lambda,析构时执行第二个lambda
    onceToken token([&]() { 
        _doing = true; 
        checkUpdate(); 
    }, [&]() { 
        checkUpdate(); 
        _doing = false; 
    });

    if (!_impl) throw std::invalid_argument("try to invoke a empty functional");
    return _impl(std::forward<ArgTypes>(args)...);
}
void checkUpdate() const {
        if (_update) {
            _update = false;
            _impl = std::move(_tmp);
        }
    }

2.3. 更新流程 (update)

当我们给 function_safe 赋值时,它会根据当前状态决定是“立即生效”还是“暂存”:

void update(func in) {
    if (!_doing) {
        // 情况A:没在执行中 -> 立即覆写 _impl
        _impl = std::move(in);
        _tmp = nullptr;
        _update = false;
    } else {
        // 情况B:正在执行中 -> 存入 _tmp,标记 _update 为 true
        _tmp = std::move(in);
        _update = true;
    }
}

效果:如果在回调内部修改了自身,新函数会被放入 _tmp。等当前回调执行完毕,onceToken 析构触发 checkUpdate(),才会将 _tmp 移动到 _impl,从而在下一次调用时生效。

3. 进阶技巧:SFINAE 与 enable_if_not_this

function_safe 的源码中,有一段看似复杂的模板定义:

template <class F>
using enable_if_not_this = typename std::enable_if<
    !std::is_same<this_type, typename std::decay<F>::type>::value, 
    typename std::decay<F>::type
>::type;

// 模板构造函数
template <typename F, typename = enable_if_not_this<F>>
function_safe(F &&f) { ... }

3.1. 为什么要这么写?

这是为了解决 通用引用(Universal Reference)构造函数与默认拷贝构造函数的竞争问题

编译器匹配规则

考虑以下代码:

function_safe<void()> a;
function_safe<void()> b(a); // 我们期望调用拷贝构造函数
  1. 默认拷贝构造函数签名通常是:function_safe(const function_safe &)
  2. 模板构造函数签名是:template<typename F> function_safe(F &&)

当我们传入 a(非 const 左值)时:

  • 调用默认拷贝构造需要一次 const 转换。
  • 调用模板构造函数,F 可以直接推导为 function_safe&,这是完美匹配

后果:编译器会优先选择模板构造函数。在模板内部,它试图将 function_safe 对象当作一个函数指针或 Lambda 去构造 std::function,这会导致编译错误(或者在某些实现下导致无限递归)。

3.2. enable_if_not_this 的作用

enable_if_not_this 利用 SFINAE(替换失败并非错误)原则:

  • 如果传入的类型 F function_safe 本身:!is_same 为假,enable_if 失败,该模板构造函数从候选列表中被剔除。编译器只能去选择默认的拷贝构造函数。
  • 如果传入的类型 F Lambda 或函数指针:模板匹配成功,使用模板构造函数进行赋值更新。

3.3. 默认拷贝构造函数

由于模板构造函数被正确限制了范围,类可以使用编译器生成的默认拷贝构造函数:

function_safe(const this_type &) = default;

它会忠实地拷贝 _impl_tmp 以及状态位,确保新对象拥有与原对象相同的行为。

4. 实战示例

下面是一个完整的示例,展示了如何在回调内部安全地“变身”:

#include "Util/util.h"
#include <iostream>

using namespace toolkit;

int main() {
    // 定义一个接受 int 参数的 function_safe
    function_safe<void(int)> safe_callback;

    // 1. 设置初始形态
    safe_callback = [&](int step) {
        std::cout << "[第一阶段] 处理步骤: " << step << std::endl;

        if (step == 1) {
            std::cout << "-> 检测到关键步骤,切换回调逻辑..." << std::endl;
            
            // 2. 在回调执行中修改自身!
            // 如果是普通 std::function,这里可能已经崩溃了
            safe_callback = [](int step) {
                std::cout << "[第二阶段] 新逻辑处理: " << step << std::endl;
            };
        }
    };

    // 调用第一次:执行第一阶段逻辑,并触发更新
    safe_callback(1); 
    
    // 调用第二次:新逻辑已生效
    safe_callback(2);

    return 0;
}

输出结果:

[第一阶段] 处理步骤: 1
-> 检测到关键步骤,切换回调逻辑...
[第二阶段] 新逻辑处理: 2

5. 总结

ZLToolKit 的 function_safe 是一个教科书式的 C++ 工具类设计:

  1. 解决实际问题:通过延迟更新机制,解决了回调重入修改的安全性问题。
  2. 利用 RAII:使用 onceToken 确保状态管理的异常安全。
  3. 精通模板编程:使用 enable_if 和 SFINAE 完美处理了构造函数重载决议问题,保证了类的易用性和正确性。

在处理复杂的异步状态机或网络协议解析时,使用 function_safe 替代原始的 std::function,能为程序的稳定性提供强有力的保障。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐