如何用函数式编程实现信号槽


概述

信号槽是 Qt 框架中用于对象间通信的一种方式。这种机制允许对象在发生特定事件时发出信号,而其他对象可以连接这些信号并提供相应的处理函数(槽),以响应这些事件。有一种熟悉的感觉对不对,其实类似于事件驱动编程,现在让我们看看如何实现。

核心概念和用途

  1. 事件驱动的通信:Qt 的信号和槽允许对象在发生特定事件时发出通知(信号),并由其他对象响应这些事件(槽)。
  2. 回调的注册与触发:在 Qt 中,你可以使用 connect 来注册槽函数,使用 emit 来触发信号。
  3. 参数传递:Qt 的信号可以带有参数,这些参数会传递给槽函数。
  4. 动态行为:Qt 的信号和槽机制是动态的,可以在运行时连接和断开。
  5. 生命周期管理:Qt 信号和槽机制可以处理对象的生命周期,例如使用 QObject 的子类。我们可以通过 std::weak_ptr 来处理可能的对象生命周期问题,防止回调尝试访问已经销毁的对象。
  6. 灵活性和通用性:Qt 的信号和槽非常灵活,可以连接任何可调用的对象。也提供了类似的灵活性,允许连接函数、lambda 表达式、bind 表达式等。
  7. 回调的执行策略:Qt 中的槽可以是排队的或直接的,可以根据需要选择。

用Cpp标准实现

函数成员

在此之前首先介绍一下封装的函数成员:

enum class CallbackResult {
    Keep,
    Erase,
};
#if __cpp_lib_move_only_function // standard feature-test macro
    using Functor = std::move_only_function<void(T...)>;
#else
    using Functor = std::function<CallbackResult(T...)>;
#endif
    std::vector<Functor> m_callbacks;

这里的 Functor 类型有两个可能的定义:

  1. 如果支持 std::move_only_function(C++23 引入):
    • 使用 std::move_only_function 作为 Functor 类型。std::move_only_function 是 C++23 新引入的类型,用于存储只可移动的函数对象。它的优势在于,一旦被移动,源对象将不再可用,这有助于避免悬垂引用等问题。
    • 这里的 std::move_only_function 模板使用 void(T...) 作为参数,表示回调函数没有返回值,接受 T... 类型的参数列表。
  2. 如果不支持 std::move_only_function:
    • 回退到使用 std::function 作为 Functor 类型。std::function 是一个通用的多态函数包装器,它可以存储、调用和复制任何可调用对象,其签名与模板参数给定的签名匹配。
    • 这里的 std::function 模板使用 CallbackResult(T...) 作为参数,表示回调函数返回 CallbackResult 类型,接受 T... 类型的参数列表。

Functor 类型在 Signal 类模板中用于:

  • 存储回调函数,允许 Signal 对象在 emit 被调用时执行这些回调。
  • 提供类型安全和灵活性,允许存储任何匹配签名的可调用对象。

Tag类

保证强类型安全。

inline constexpr struct once_flag_t {
    explicit once_flag_t() = default;
} once_flag;

enum class nth_flag_t : size_t {};

connect函数

这里展示的是最终重载的版本,对于connect函数,提供了灵活的方式来注册不同类型的回调函数,无论是独立的函数还是对象的成员函数,都可以很容易地连接到 Signal 对象上。

1. 接受可调用对象的 connect 函数模板

template<class Func>
void connect(Func cb) {
    if constexpr (std::is_invocable_r_v<CallbackResult, Func, T...>) {
        // 如果回调的返回类型是 CallbackResult,直接添加到回调列表
        m_callbacks.push_back(std::move(cb));
    } else if constexpr (std::is_invocable_r_v<bool, Func, T...>) {
        // 如果回调的返回类型是 bool,包装回调使其返回 CallbackResult 类型
        m_callbacks.push_back([ca = std::move(cb)](T&&...args) {
            if (ca(std::forward<T>(args)...)) {
                return CallbackResult::Erase;
            }
            return CallbackResult::Keep;
        });
    } else {
        // 默认情况,假设回调不需要特殊处理返回值
        m_callbacks.push_back([ca = std::move(cb)](T&&...args) {
            ca(std::forward<T>(args)...);
            return CallbackResult::Keep;
        });
    }
}
  • 这个 connect 函数接受一个可调用对象 cb,例如函数、lambda 表达式或 std::function 对象。
  • 它使用 if constexprstd::is_invocable_r_v 来检查 cb 的返回类型是否为 CallbackResultbool
    • 如果返回类型是 CallbackResult,直接将 cb 移动到回调列表 m_callbacks 中。
    • 如果返回类型是 bool,创建一个新的 lambda 表达式,它调用原始回调 ca 并根据 ca 的返回值转换为 CallbackResult 类型。
    • 如果回调的返回类型既不是 CallbackResult 也不是 bool,则假设回调不需要处理返回值,并创建一个新的 lambda 表达式,它调用原始回调并返回 CallbackResult::Keep

2. 接受成员函数和对象的 connect 函数模板

template<class Self, class MemFn, class... Tag>
void connect(Self self, MemFn mem_fn, Tag... tag) {
    m_callbacks.push_back(detail_::bind(std::move(self), mem_fn, tag...));
}
  • 这个 connect 函数接受一个对象 self、一个成员函数指针 mem_fn 和一些标签参数 Tag...
  • 它使用 detail_::bind 函数来创建一个绑定到成员函数的回调,并将这个回调添加到回调列表 m_callbacks 中。
  • detail_::bind 根据传入的标签参数 Tag... 创建不同类型的绑定,例如一次性执行或按次数执行的回调。

这两个 connect 函数模板提供了灵活的方式来注册不同类型的回调函数,无论是独立的函数还是对象的成员函数。

bind函数

bind可以说是整个函数的灵魂,它的作用是创建一个闭包(通常是 lambda 表达式),该闭包在被调用时,会调用与某个对象相关联的成员函数。这个 bind 函数有几个重载版本,用于不同的绑定场景,比如一次性调用、按次数调用,以及处理弱引用(std::weak_ptr)的情况。

首先是关于weak_ptr的判断:

    template<class Self>
    std::shared_ptr<Self> lock_if_weak(std::weak_ptr<Self> const &self) {
        return self.lock();
    }

    template<class Self>
    Self const &lock_if_weak(Self const &self) {
        return self;
    }

1. 一次性调用的 bind 函数模板

template<class Self, class MemFn>
auto bind(Self self, MemFn mem_fn, once_flag_t) {
    return [self = std::move(self), mem_fn](auto... args) {
        auto const &ptr = lock_if_weak(self);
        if (ptr == nullptr) return CallbackResult::Erase;
        ((*ptr).*mem_fn)(args...);
        return CallbackResult::Erase;
    };
}
  • 这个版本接受一个 Self 类型的对象 self,一个成员函数指针 MemFn,以及一个 once_flag_t 类型的标记。
  • 创建的闭包在第一次调用时会执行成员函数,然后返回 CallbackResult::Erase,表示这个回调应该从回调列表中删除。

2. 按次数调用的 bind 函数模板

template<class Self, class MemFn>
auto bind(Self self, MemFn mem_fn, nth_flag_t nth) {
    return [self = std::move(self), mem_fn,
            nth = static_cast<int>(nth)](auto... args) mutable {
        if (nth <= 0) { return CallbackResult::Erase; }
        auto const &ptr = lock_if_weak(self);
        if (ptr == nullptr) return CallbackResult::Erase;
        ((*ptr).*mem_fn)(args...);
        nth--;
        if (nth <= 0) return CallbackResult::Erase;
        return CallbackResult::Keep;
    };
}
  • 这个版本接受一个 Self 类型的对象 self,一个成员函数指针 MemFn,以及一个 nth_flag_t 类型的参数,表示回调应该被调用的次数。
  • 创建的闭包会根据 nth 参数的值来决定是否继续保留回调。每次调用时,nth 都会减一,当 nth 减到 0 或以下时,回调会被删除。

3. 常规 bind 函数模板

template<class Self, class MemFn>
auto bind(Self self, MemFn mem_fn) {
    return [self = std::move(self), mem_fn](auto... args) {
        auto const &ptr = lock_if_weak(self);
        if (ptr == nullptr) return CallbackResult::Erase;
        ((*ptr).*mem_fn)(args...);
        return CallbackResult::Keep;
    };
}
  • 这是最基本的 bind 版本,接受一个 Self 类型的对象 self 和一个成员函数指针 MemFn
  • 创建的闭包在每次调用时都会执行成员函数,并返回 CallbackResult::Keep,表示回调应该保留在回调列表中。

通用特性

  • 所有 bind 函数模板都会创建一个 lambda 表达式,该表达式捕获 selfmem_fn
  • self 可能是一个 std::weak_ptrlock_if_weak 函数用于尝试获取一个强引用(std::shared_ptr)。
  • 如果 lock_if_weak 返回 nullptr,表示原始对象已经被销毁,回调应该被删除。
  • 成员函数通过 ((*ptr).*mem_fn)(args...) 语法调用,args... 是传递给闭包的参数,使用 std::forward 来保持它们的值类别(左值或右值)。

bind 函数的目的是将成员函数与对象绑定,创建一个可以存储在 Signal 类中的回调。这允许 Signal 类的用户将成员函数作为回调连接到信号上,而不必担心对象的生命周期问题。

为什么按指捕获

按值捕获(value capture)意味着 lambda 表达式会复制捕获的变量到其闭包环境中。这与按引用捕获(reference capture)相对,后者仅捕获对原始变量的引用。

  • 对象的生命周期管理

使用按值捕获对于对象的生命周期管理至关重要,尤其是当捕获 std::weak_ptr 时:

auto bind(Self self, MemFn mem_fn, nth_flag_t nth) {
    return [self = std::move(self), mem_fn, nth](...) {
        // ...
    };
}

这里,self 是按值捕获的,这意味着 std::weak_ptr 被移动到 lambda 表达式中,并存储为一个局部变量。当 lambda 被调用时,它尝试通过 lock_if_weak 获取一个 std::shared_ptr。如果 self 已经无效(即原始对象已经被销毁),则 lock 将返回 nullptr,这时 lambda 可以决定不执行成员函数或执行一些清理工作。

  • 避免悬空引用

按值捕获避免了悬空引用的问题。如果使用按引用捕获,lambda 将直接引用 self,如果 self 在 lambda 调用之前被销毁,那么 lambda 中的引用将变成悬空引用,这可能导致未定义行为。

  • 确保数据的独立性

按值捕获确保了每个 lambda 表达式实例拥有其捕获变量的独立副本。这在多线程环境中特别重要,因为它避免了因多个线程访问同一资源而导致的数据竞争问题。

  • 可移动性

使用 std::move 与按值捕获结合使用,可以确保资源(如智能指针)的所有权在捕获时被转移给 lambda,而不是共享。这有助于防止资源的意外复制或多次释放。

  • 模板参数的灵活性

在模板编程中,按值捕获提供了更大的灵活性,因为模板可以处理不同类型的参数,包括那些没有默认构造函数或不能被复制的类型。在下面这个示例中,通过按值捕获 std::shared_ptr 来避免悬空引用,并且 weakSelf 只在 lambda 内部有效。总之,按值捕获在 signal.h 中的 bind 函数模板中用于确保 lambda 表达式安全地处理可能具有复杂生命周期的对象,同时保持代码的简洁性和灵活性。

std::weak_ptr<SomeClass> weakPtr = ...;
auto callback = [weakSelf = weakPtr.lock()]() {
    if (weakSelf) {
        // 使用 weakSelf 做一些操作
    }
};

模板函数省略的版本

// 避免重复声明多个版本
template<class Self, class MemFn>
auto bind_once(Self self, MemFn mem_fn) {
    return [self = std::move(self), mem_fn](auto... args) {
        ((*self).*mem_fn)(args...);
        return CallbackResult::Erase;
    };
}
template<class Self, class MemFn>
auto bind_nth(Self self, MemFn mem_fn, size_t nth = 10) {
    return [self = std::move(self), mem_fn, nth](auto... args) mutable {
        if (nth <= 0) { return CallbackResult::Erase; }
        ((*self).*mem_fn)(args...);
        nth--;
        if (nth <= 0) return CallbackResult::Erase;
        return CallbackResult::Keep;
    };
}
template<class Self, class MemFn>
auto bind(std::weak_ptr<Self> self, MemFn mem_fn) {
    return [self = std::move(self), mem_fn](auto... args) {
        auto ptr = self.lock();
        if (ptr == nullptr) { return CallbackResult::Erase; }
        ((*ptr).*mem_fn)(args...);
        return CallbackResult::Keep;
    };
}
 template<class Self, class MemFn>
 void connect_once(Self self, MemFn mem_fn) {
     m_callbacks.push_back(bind_once(std::move(self), mem_fn));
 }

template<class Self, class MemFn>
void connect_nth(Self self, MemFn mem_fn, size_t nth = 10) {
    m_callbacks.push_back(bind_nth(std::move(self), mem_fn, nth));
}

template<class Self, class MemFn>
void connect_weak(std::shared_ptr<Self> self, MemFn mem_fn) {
    m_callbacks.push_back(bind(std::weak_ptr<Self>(self), mem_fn));
}

文章作者: JoyTsing
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 JoyTsing !
评论
  目录