当前位置: 首页 > news >正文

C++实现一个简单的Qt信号槽机制

昨天写这个文章《深入探讨C++的高级反射机制(2):写个能用的反射库》的时候就在想,是不是也能在这套反射逻辑的基础上,实现一个类似Qt的信号槽机制?

Qt信号槽机制简介

所谓的Qt的信号槽(Signals and Slots)机制,是Qt框架中实现对象之间通信的一种方式。这是一个事件驱动程序中常见的设计模式。信号槽机制允许创建响应特定事件(如用户交互、数据变化等)的可重用组件。
信号槽主要有以下核心概念组成:

信号(Signals)

信号是一个类成员函数的声明,它在类内部以 signals: 关键词标识。当某个事件发生时,可以发射(emit)信号。信号不包含具体的实现代码,只是一个通知机制。它告诉外界某个事件已经发生,比如按钮被点击或者定时器超时。

槽(Slots)

槽是一个普通的成员函数,可以是公有的、保护的或私有的,它在类内部以 slots: 关键词标识(Qt 5 开始,普通的成员函数也可以作为槽)。槽函数包含了当信号发射时应该执行的代码。换句话说,槽函数是对信号的响应。

连接(Connection)

信号和槽之间的连接是通过 QObject::connect() 函数建立的。这个连接指定了当信号发射时,应该调用哪个槽函数。一个信号可以连接到多个槽,一个槽也可以接收来自多个信号的通知。

示例

以下是一个简单的Qt信号和槽的例子,展示了这个机制如何工作:

#include <QObject>class Button : public QObject {Q_OBJECTpublic:Button() {}signals:void clicked(); // 信号声明public slots:void onClick() { // 槽声明// 处理按钮点击事件}
};int main() {Button button;// 连接按钮的 clicked 信号到同一个按钮的 onClick 槽QObject::connect(&button, &Button::clicked, &button, &Button::onClick);// 在某个地方,按钮被点击,发射信号emit button.clicked();return 0;
}#include "main.moc" // 如果使用qmake或CMake,通常不需要这一行

在这个例子中,当按钮被点击时,它会发射 clicked 信号,这会导致调用与它连接的 onClick 槽函数。

信号槽机制的优点在于它提供了一种松耦合的方式来处理事件。对象不需要知道哪些对象或函数对它们的信号感兴趣,它们只需在合适的时候发射信号。这样可以创建可重用和可维护的组件,同时简化了应用程序的事件处理逻辑。

我们的实现思路

为了实现类似于Qt信号槽的机制,我们需要一个类似QObject的基类。为了避免引入新概念,我们这个类也直接较QObject好了。类中实现信号的发射(emit)和槽的连接(connect)。
笔者不太喜欢Qt的connect函数是个静态函数,所以我们这里的实现稍微和Qt不一样,我们的connect函数是个普通成员函数,用于将自己的信号连接到目标槽上。
接下来,我们需要声明信号的机制。我们通过定义宏DECL_SIGNAL来声明一个信号,并实现相应的连接和断开连接的逻辑。
于是,我们的信号槽大概用法如下:


// 用户自定义的结构体
class MyStruct : public refl::QObject // 信号槽等功能从这个类派生
{
public:// 定义一个方法,用作槽函数,必须在REFLECTABLE_MENBER_FUNCS列表中,并且参数必须是std::any,不能超过4个参数。std::any on_x_value_modified(std::any new_value) {int value = std::any_cast<int>(new_value);std::cout << "MyStruct::on_x_value_modified called! New value is: " << value << std::endl;return 0;}REFLECTABLE_MENBER_FUNCS(MyStruct,REFLEC_FUNCTION(on_x_value_modified));DECL_SIGNAL(x_value_modified, int) // 声明信号x_value_modifiedDECL_DYNAMIC_REFLECTABLE(MyStruct)//动态反射的支持
};// 信号槽的连接和调用:MyStruct obj1;
MyStruct obj2;// 连接obj1的信号到obj2的槽函数
size_t connection_id = obj1.**connect**("x_value_modified", &obj2, "on_x_value_modified");
if (connection_id != 0) {std::cout << "Signal x_value_modified from obj1 connected to on_x_value_modified slot in obj2." << std::endl;
}
obj1.x_value_modified(42);// 触发信号// 断开连接
obj1.**disconnect**(connection_id);
// 再次触发信号,应该没有任何输出,因为已经断开连接
obj1.x_value_modified(84);

有了用法的情况下,我们就有了目标了。
这个是我们DECL_SIGNAL和QObject的实现:

//宏用于类中声明信号,并提供一个同名的方法来触发信号。#define DECL_SIGNAL(signal_name, ...) \template<typename... Args> \void signal_name(Args&&... args) { \emit_signal_impl(#signal_name, std::forward<Args>(args)...); \} \
class QObject : public refl::dynamic::IReflectable {private:// 信号与槽的映射,键是信号名称,值是一组槽函数的信息std::unordered_map<std::string, std::vector<std::pair<QObject*, std::string>>> connections;size_t next_connection_id = 1;std::map<size_t, std::pair<std::string, std::pair<QObject*, std::string>>> connection_map;public:template<typename... Args>void emit_signal_impl(const char* signal_name, Args&&... args) {auto it = connections.find(signal_name);if (it != connections.end()) {for (auto& slot_info : it->second) {slot_info.first->invoke_member_func_by_name(slot_info.second.c_str(), std::forward<Args>(args)...);//invoke_member_func_type_safe(*slot_info.first, slot_info.second.c_str(), std::forward<Args>(args)...); }}}size_t connect(const char* signal_name, QObject* target, const char* target_member_func_name) {if (!target || !signal_name || !target_member_func_name) return 0;connections[signal_name].emplace_back(target, target_member_func_name);size_t id = next_connection_id++;connection_map[id] = { signal_name, {target, target_member_func_name} };return id;}bool disconnect(size_t connection_id) {auto it = connection_map.find(connection_id);if (it != connection_map.end()) {auto& [signal_name, slot_info] = it->second;auto& slots = connections[signal_name];slots.erase(std::remove(slots.begin(), slots.end(), slot_info), slots.end());connection_map.erase(it);return true;}return false;}};

运行起来,还不错:
在这里插入图片描述

但是这段代码很不优雅:

size_t connection_id = obj1.connect("x_value_modified", &obj2, "on_x_value_modified");

因为都是字符串,万一打错了单词还不容易发现。我们是否可以优化成这种形式:

size_t connection_id = obj1.connect(&MyStruct::x_value_modified, &obj2, &MyStruct::on_x_value_modified);

实现这种形式也不难,我们需要对connect方法进行重载,使其能接受成员函数指针而不是字符串。并能从成员函数指针中提取其函数名称。

template <typename SignalClass, typename SignalType, typename SlotClass, typename SlotType>size_t connect(SignalType SignalClass::*signal, SlotClass* slot_instance, SlotType SlotClass::*slot) {const char* signal_name = get_member_func_name<SignalClass>(signal);const char* slot_name = get_member_func_name<SlotClass>(slot);if (signal_name && slot_name) {return connect(signal_name, static_cast<QObject*>(slot_instance), slot_name);}return 0; // Failed}

由于我们已经有了之前反射库的实现经验,get_member_func_name的实现也信手拈来:

template <typename T, typename FuncTuple, size_t N = 0>
constexpr const char* __get_member_func_name_impl(void* func_ptr, const FuncTuple& tp) {if constexpr (N >= std::tuple_size_v<FuncTuple>) {return nullptr; // Not Found!} else {const auto& func = std::get<N>(tp);if (reinterpret_cast<void*>(func.get_func()) == func_ptr) {return func.name;} else {return __get_member_func_name_impl<T, FuncTuple, N + 1>(func_ptr, tp);}}
}template <typename T, typename FuncPtr>
constexpr const char* get_member_func_name(FuncPtr func_ptr) {constexpr auto funcs = T::member_funcs();return __get_member_func_name_impl<T>(reinterpret_cast<void*>(func_ptr), funcs);
}

不过编译下来,发现这种做法有点问题,前面DECL_SIGNAL声明的是一个变参模板函数,导致无法对其进行取地址:

#define DECL_SIGNAL(signal_name, ...) \template<typename... Args> \void signal_name(Args&&... args) { \emit_signal_impl(#signal_name, std::forward<Args>(args)...); \} \

直接取地址是会报错的:
在这里插入图片描述
经过一轮思索,于是把DECL_SIGNAL的使用形式改为:IMPL_SIGNAL:

	//宏用于类中声明信号,并提供一个同名的方法来触发信号。示例:/*	void x_value_modified(int param) {IMPL_SIGNAL(param);}*/#define IMPL_SIGNAL(...) raw_emit_signal_impl(__func__ , __VA_ARGS__)

于是,前面的类声明信号的部分由

DECL_SIGNAL(x_value_modified, int) // 声明信号x_value_modified

变为:

void x_value_modified(int param) {IMPL_SIGNAL(param);}

新的定义提供了更好的类型安全保障,避免参数个数和类型传错了导致发射信号失败。同时兼容我们更安全的connect的版本。
在这里插入图片描述

好了,就先这样吧。以后有时间继续优化。

这次完整的代码如下:

#include <iostream>
#include <tuple>
#include <stdexcept>
#include <assert.h>
#include <string_view>
#include <optional>
#include <utility> // For std::forward
#include <unordered_map>
#include <functional>
#include <memory>
#include <any>
#include <type_traits> // For std::is_invocable
#include <map>namespace refl {// 这个宏用于创建字段信息
#define REFLECTABLE_PROPERTIES(TypeName, ...)  using CURRENT_TYPE_NAME = TypeName; \static constexpr auto properties() { return std::make_tuple(__VA_ARGS__); }
#define REFLECTABLE_MENBER_FUNCS(TypeName, ...) using CURRENT_TYPE_NAME = TypeName; \static constexpr auto member_funcs() { return std::make_tuple(__VA_ARGS__); }// 这个宏用于创建属性信息,并自动将字段名转换为字符串
#define REFLEC_PROPERTY(Name) refl::Property<decltype(&CURRENT_TYPE_NAME::Name), &CURRENT_TYPE_NAME::Name>(#Name)
#define REFLEC_FUNCTION(Func) refl::Function<decltype(&CURRENT_TYPE_NAME::Func), &CURRENT_TYPE_NAME::Func>(#Func)// 定义一个属性结构体,存储字段名称和值的指针template <typename T, T Value>struct Property {const char* name;constexpr Property(const char* name) : name(name) {}constexpr T get_value() const { return Value; }};template <typename T, T Value>struct Function {const char* name;constexpr Function(const char* name) : name(name) {}constexpr T get_func() const { return Value; }};// 使用 std::any 来处理不同类型的字段值和函数返回值template <typename T, typename Tuple, size_t N = 0>std::any __get_field_value_impl(T& obj, const char* name, const Tuple& tp) {if constexpr (N >= std::tuple_size_v<Tuple>) {return std::any();// Not Found!}else {const auto& prop = std::get<N>(tp);if (std::string_view(prop.name) == name) {return std::any(obj.*(prop.get_value()));}else {return __get_field_value_impl<T, Tuple, N + 1>(obj, name, tp);}}}// 使用 std::any 来处理不同类型的字段值和函数返回值template <typename T, size_t N = 0>std::any get_field_value(T& obj, const char* name) {return __get_field_value_impl(obj, name, T::properties());}// 使用 std::any 来处理不同类型的字段值和函数返回值template <typename T, typename Tuple, typename Value, size_t N = 0>std::any __assign_field_value_impl(T& obj, const char* name, const Value& value, const Tuple& tp) {if constexpr (N >= std::tuple_size_v<Tuple>) {return std::any();// Not Found!}else {const auto& prop = std::get<N>(tp);if (std::string_view(prop.name) == name) {if constexpr (std::is_assignable_v<decltype(obj.*(prop.get_value())), Value>) {obj.*(prop.get_value()) = value;return std::any(obj.*(prop.get_value()));}else {assert(false);// 无法赋值 类型不匹配!!return std::any();}}else {return __assign_field_value_impl<T, Tuple, Value, N + 1>(obj, name, value, tp);}}}template <typename T, typename Value>std::any assign_field_value(T& obj, const char* name, const Value& value) {return __assign_field_value_impl(obj, name, value, T::properties());}// 成员函数调用相关:template <bool assert_when_error = true, typename T, typename FuncTuple, size_t N = 0, typename... Args>constexpr std::any __invoke_member_func_impl(T& obj, const char* name, const FuncTuple& tp, Args&&... args) {if constexpr (N >= std::tuple_size_v<FuncTuple>) {assert(!assert_when_error);// 没找到!return std::any();// Not Found!}else {const auto& func = std::get<N>(tp);if (std::string_view(func.name) == name) {if constexpr (std::is_invocable_v<decltype(func.get_func()), T&, Args...>) {if constexpr (std::is_void<decltype(std::invoke(func.get_func(), obj, std::forward<Args>(args)...))>::value) {// 如果函数返回空,那么兼容这种casestd::invoke(func.get_func(), obj, std::forward<Args>(args)...);return std::any();}else {return std::invoke(func.get_func(), obj, std::forward<Args>(args)...);}}else {assert(!assert_when_error);// 调用参数不匹配return std::any();}}else {return __invoke_member_func_impl<assert_when_error, T, FuncTuple, N + 1>(obj, name, tp, std::forward<Args>(args)...);}}}template <typename T, typename... Args>constexpr std::any invoke_member_func(T& obj, const char* name, Args&&... args) {constexpr auto funcs = T::member_funcs();return __invoke_member_func_impl(obj, name, funcs, std::forward<Args>(args)...);}template <typename T, typename... Args>constexpr std::any invoke_member_func_safe(T& obj, const char* name, Args&&... args) {constexpr auto funcs = T::member_funcs();return __invoke_member_func_impl<true>(obj, name, funcs, std::forward<Args>(args)...);}template <typename T, typename FuncPtr, typename FuncTuple, size_t N = 0>constexpr const char* __get_member_func_name_impl(FuncPtr func_ptr, const FuncTuple& tp) {if constexpr (N >= std::tuple_size_v<FuncTuple>) {return nullptr; // Not Found!}else {const auto& func = std::get<N>(tp);if constexpr (std::is_same< decltype(func.get_func()), FuncPtr >::value) {return func.name;}else {return __get_member_func_name_impl<T, FuncPtr, FuncTuple, N + 1>(func_ptr, tp);}}}template <typename T, typename FuncPtr>constexpr const char* get_member_func_name(FuncPtr func_ptr) {constexpr auto funcs = T::member_funcs();return __get_member_func_name_impl<T, FuncPtr>(func_ptr, funcs);}// 定义一个类型特征模板,用于获取属性信息template <typename T>struct For {static_assert(std::is_class_v<T>, "Reflector requires a class type.");// 遍历所有字段名称template <typename Func>static void for_each_propertie_name(Func&& func) {constexpr auto props = T::properties();std::apply([&](auto... x) {((func(x.name)), ...);}, props);}// 遍历所有字段值template <typename Func>static void for_each_propertie_value(T& obj, Func&& func) {constexpr auto props = T::properties();std::apply([&](auto... x) {((func(x.name, obj.*(x.get_value()))), ...);}, props);}// 遍历所有函数名称template <typename Func>static void for_each_member_func_name(Func&& func) {constexpr auto props = T::member_funcs();std::apply([&](auto... x) {((func(x.name)), ...);}, props);}};// ===============================================================// 以下是动态反射机制的支持代码:namespace dynamic {// 反射基类class IReflectable {public:virtual ~IReflectable() = default;virtual std::string_view get_type_name() const = 0;virtual std::any get_field_value_by_name(const char* name) const = 0;virtual std::any invoke_member_func_by_name(const char* name) = 0;virtual std::any invoke_member_func_by_name(const char* name, std::any param1) = 0;virtual std::any invoke_member_func_by_name(const char* name, std::any param1, std::any param2) = 0;virtual std::any invoke_member_func_by_name(const char* name, std::any param1, std::any param2, std::any param3) = 0;virtual std::any invoke_member_func_by_name(const char* name, std::any param1, std::any param2, std::any param3, std::any param4) = 0;// 不能无限增加,会增加虚表大小。最多支持4个参数的调用。};// 类型注册工具class TypeRegistry {public:using CreatorFunc = std::function<std::unique_ptr<IReflectable>()>;static TypeRegistry& instance() {static TypeRegistry registry;return registry;}void register_type(const std::string_view type_name, CreatorFunc creator) {creators[type_name] = std::move(creator);}std::unique_ptr<IReflectable> create(const std::string_view type_name) {if (auto it = creators.find(type_name); it != creators.end()) {return it->second();}return nullptr;}private:std::unordered_map<std::string_view, CreatorFunc> creators;};// 用于注册类型信息的宏
#define DECL_DYNAMIC_REFLECTABLE(TypeName) \friend class refl::dynamic::TypeRegistryEntry<TypeName>; \static std::string_view static_type_name() { return #TypeName; } \virtual std::string_view get_type_name() const override { return static_type_name(); } \static std::unique_ptr<::refl::dynamic::IReflectable> create_instance() { return std::make_unique<TypeName>(); } \static const bool is_registered; \std::any get_field_value_by_name(const char* name) const override { \return refl::get_field_value(*this, name); \} \std::any invoke_member_func_by_name(const char* name) override { \return refl::invoke_member_func(*static_cast<TypeName*>(this), name); \}\std::any invoke_member_func_by_name(const char* name, std::any param1) override { \return refl::invoke_member_func(*static_cast<TypeName*>(this), name, param1); \}\std::any invoke_member_func_by_name(const char* name, std::any param1, std::any param2) override { \return refl::invoke_member_func(*static_cast<TypeName*>(this), name, param1, param2); \}\std::any invoke_member_func_by_name(const char* name, std::any param1, std::any param2, std::any param3) override { \return refl::invoke_member_func(*static_cast<TypeName*>(this), name, param1, param2, param3); \}\std::any invoke_member_func_by_name(const char* name, std::any param1, std::any param2, std::any param3, std::any param4) override { \return refl::invoke_member_func(*static_cast<TypeName*>(this), name, param1, param2, param3, param4); \}\
// 用于在静态区域注册类型的辅助类template <typename T>class TypeRegistryEntry {public:TypeRegistryEntry() {::refl::dynamic::TypeRegistry::instance().register_type(T::static_type_name(), &T::create_instance);}};// 为每个类型定义注册变量,这段宏需要出现在cpp中。
#define REGEDIT_DYNAMIC_REFLECTABLE(TypeName) \const bool TypeName::is_registered = [] { \static ::refl::dynamic::TypeRegistryEntry<TypeName> entry; \return true; \}();}//namespace dynamic//宏用于类中声明信号,并提供一个同名的方法来触发信号。示例:/*	void x_value_modified(int param) {IMPL_SIGNAL(param);}*/
#define IMPL_SIGNAL(...) raw_emit_signal_impl(__func__ , __VA_ARGS__)class QObject : public refl::dynamic::IReflectable {private:// 信号与槽的映射,键是信号名称,值是一组槽函数的信息std::unordered_map<std::string, std::vector<std::pair<QObject*, std::string>>> connections;size_t next_connection_id = 1;std::map<size_t, std::pair<std::string, std::pair<QObject*, std::string>>> connection_map;public:template<typename... Args>void raw_emit_signal_impl(const char* signal_name, Args&&... args) {auto it = connections.find(signal_name);if (it != connections.end()) {for (auto& slot_info : it->second) {slot_info.first->invoke_member_func_by_name(slot_info.second.c_str(), std::forward<Args>(args)...);//invoke_member_func_type_safe(*slot_info.first, slot_info.second.c_str(), std::forward<Args>(args)...); }}else {assert(false);}}size_t connect(const char* signal_name, QObject* target, const char* target_member_func_name) {if (!target || !signal_name || !target_member_func_name) return 0;connections[signal_name].emplace_back(target, target_member_func_name);size_t id = next_connection_id++;connection_map[id] = { signal_name, {target, target_member_func_name} };return id;}template <typename SignalClass, typename SignalType, typename SlotClass, typename SlotType>size_t connect(SignalType SignalClass::* signal, SlotClass* slot_instance, SlotType SlotClass::* slot) {const char* signal_name = get_member_func_name<SignalClass>(signal);const char* slot_name = get_member_func_name<SlotClass>(slot);if (signal_name && slot_name) {return connect(signal_name, static_cast<QObject*>(slot_instance), slot_name);}return 0; // Failed}bool disconnect(size_t connection_id) {auto it = connection_map.find(connection_id);if (it != connection_map.end()) {auto& [signal_name, slot_info] = it->second;auto& slots = connections[signal_name];slots.erase(std::remove(slots.begin(), slots.end(), slot_info), slots.end());connection_map.erase(it);return true;}return false;}};}// namespace refl// =========================一下为使用示例代码====================================// 用户自定义的结构体
class MyStruct ://public refl::dynamic::IReflectable 	// 如果不需要动态反射,可以不从public refl::dynamic::IReflectable派生public refl::QObject // 这里我们也测试信号槽等功能,因此从这个类派生
{public:int x{ 10 };double y{ 20.5f };int print() const {std::cout << "MyStruct::print called! " << "x: " << x << ", y: " << y << std::endl;return 666;}// 如果需要支持动态调用,参数必须是std::any,并且不能超过4个参数。int print_with_arg(std::any param) const {std::cout << "MyStruct::print called! " << " arg is: " << std::any_cast<int>(param) << std::endl;return 888;}// 定义一个方法,用作槽函数,必须在REFLECTABLE_MENBER_FUNCS列表中,不支持返回值,并且参数必须是std::any,不能超过4个参数。std::any on_x_value_modified(std::any& new_value) {int value = std::any_cast<int>(new_value);std::cout << "MyStruct::on_x_value_modified called! New value is: " << value << std::endl;return 0;}void x_value_modified(std::any param) {IMPL_SIGNAL(param);}REFLECTABLE_PROPERTIES(MyStruct,REFLEC_PROPERTY(x),REFLEC_PROPERTY(y));REFLECTABLE_MENBER_FUNCS(MyStruct,REFLEC_FUNCTION(print),REFLEC_FUNCTION(print_with_arg),REFLEC_FUNCTION(on_x_value_modified),REFLEC_FUNCTION(x_value_modified));DECL_DYNAMIC_REFLECTABLE(MyStruct)//动态反射的支持,如果不需要动态反射,可以去掉这行代码
};//动态反射注册类
REGEDIT_DYNAMIC_REFLECTABLE(MyStruct)int main() {MyStruct obj;// # 静态反射部分:// 打印所有字段名称refl::For<MyStruct>::for_each_propertie_name([](const char* name) {std::cout << "Field name: " << name << std::endl;});// 打印所有字段值refl::For<MyStruct>::for_each_propertie_value(obj, [](const char* name, auto&& value) {std::cout << "Field " << name << " has value: " << value << std::endl;});// 打印所有函数名称refl::For<MyStruct>::for_each_member_func_name([](const char* name) {std::cout << "Member func name: " << name << std::endl;});// 获取特定成员的值,如果找不到成员,则返回默认值auto x_value = refl::get_field_value(obj, "x");std::cout << "Field x has value: " << std::any_cast<int>(x_value) << std::endl;auto y_value = refl::get_field_value(obj, "y");std::cout << "Field y has value: " << std::any_cast<double>(y_value) << std::endl;//修改值:refl::assign_field_value(obj, "y", 33.33f);y_value = refl::get_field_value(obj, "y");std::cout << "Field y has modifyed,new value is: " << std::any_cast<double>(y_value) << std::endl;auto z_value = refl::get_field_value(obj, "z"); // "z" 不存在if (z_value.type().name() == std::string_view("int")) {std::cout << "Field z has value: " << std::any_cast<int>(z_value) << std::endl;}// 通过字符串调用成员函数 'print'auto print_ret = refl::invoke_member_func_safe(obj, "print");std::cout << "print member return: " << std::any_cast<int>(print_ret) << std::endl;std::cout << "---------------------动态反射部分:" << std::endl;// 动态反射部分(动态反射完全不需要知道类型MyStruct的定义):// 动态创建 MyStruct 实例并调用方法auto instance = refl::dynamic::TypeRegistry::instance().create("MyStruct");if (instance) {std::cout << "Dynamic instance type: " << instance->get_type_name() << std::endl;// 这里可以调用 MyStruct 的成员方法auto x_value2 = instance->get_field_value_by_name("x");std::cout << "Field x has value: " << std::any_cast<int>(x_value2) << std::endl;instance->invoke_member_func_by_name("print");instance->invoke_member_func_by_name("print_with_arg", 10);//instance->invoke_member_func_by_name("print_with_arg", 20, 222);//这个调用会失败,命中断言,因为print_with_arg只接受一个函数}// 信号槽部分:std::cout << "---------------------信号槽部分:" << std::endl;MyStruct obj1;MyStruct obj2;// 连接obj1的信号到obj2的槽函数size_t connection_id = obj1.connect("x_value_modified", &obj2, "on_x_value_modified");if (connection_id != 0) {std::cout << "Signal x_value_modified from obj1 connected to on_x_value_modified slot in obj2." << std::endl;}obj1.x_value_modified(42);// 触发信号// 断开连接obj1.disconnect(connection_id);// 再次触发信号,应该没有任何输出,因为已经断开连接obj1.x_value_modified(84);// 使用成员函数指针版本的connectconnection_id = obj1.connect(&MyStruct::x_value_modified, &obj2, &MyStruct::on_x_value_modified);if (connection_id != 0) {std::cout << "Signal connected to slot." << std::endl;}obj1.x_value_modified(666);// 触发信号return 0;
}

相关文章:

C++实现一个简单的Qt信号槽机制

昨天写这个文章《深入探讨C的高级反射机制&#xff08;2&#xff09;&#xff1a;写个能用的反射库》的时候就在想&#xff0c;是不是也能在这套反射逻辑的基础上&#xff0c;实现一个类似Qt的信号槽机制&#xff1f; Qt信号槽机制简介 所谓的Qt的信号槽&#xff08;Signals …...

微信小程序常用的传值

1.通过 URL 传参 在页面跳转时&#xff0c;可以在 URL 中携带参数进行传递&#xff0c;然后在目标页面的 onLoad 生命周期中获取参数。 // 在页面 A 中跳转到页面 B 并传递参数 wx.navigateTo({url: /pages/detail/index?id123 });// 在页面 B 的 onLoad 生命周期中获取参数…...

SQL面试真题解答 数据统计分析,求“同比、环比”等(SQL窗口函数使用)

SQL面试真题解答 数据统计分析&#xff0c;求“同比、环比”等&#xff08;SQL窗口函数使用&#xff09; 环比、环比增长率、同比、同比增长率&#xff0c;根据百度百科上的 说明&#xff1a; 环比增长率 环比增长率&#xff0c;一般是指和上期相比较的增长率。 环比增长率&a…...

【递归、搜索与回溯】floodfill算法二

floodfill算法二 1.被围绕的区域2.太平洋大西洋水流问题3.扫雷游戏4.衣橱整理 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496; 你的支持是对我最大的鼓励&#xff0c;我们一起努力吧!&#x1f603;&#x1f603; 1.被围绕的区域…...

Dataease安装,配置Jenkins自动部署

Dataease安装&#xff0c;配置Jenkins自动部署 一.安装Dataease 安装前准备&#xff1a;1.Ubuntu20.04 LTS国内源安装指定版本Docker 2.docker-compose安装 下载离线安装的安装包&#xff0c;下载地址&#xff1a;https://community.fit2cloud.com/#/download/dataease/v1-…...

关于IDEA启动报错 【JAVA_HOME does not point to a valid JM installation】

希望文章能给到你启发和灵感&#xff5e; 感谢支持和关注&#xff5e; 阅读指南 一、基础环境说明1.1 硬件环境1.2 软件环境 二、起因 一、基础环境说明 考虑环境因素不同&#xff0c;大家适当的对比自己的软硬件环境情况分析&#xff5e; 1.1 硬件环境 MacOS Monterey 版本 1…...

设置小蓝熊的CPU亲和性、CPU优先级再设置法环的CPU亲和性

# 适用于Windows系统 # 时间 : 2024-06-28 # 作者 : 三巧(https://blog.csdn.net/qq_39124701) # 文件名 : 设置小蓝熊的CPU亲和性、CPU优先级再设置法环的CPU亲和性.ps1 # 使用方法: 打开记事本&#xff0c;将所有代码复制到记事本中&#xff0c;保存文件时候修改文件后…...

Oracle中的序列(Sequence)是一种数据库对象

Oracle中的序列&#xff08;Sequence&#xff09;是一种数据库对象&#xff0c;用于生成数字序列&#xff0c;通常用于为主键列生成唯一、连续的数值。以下是一些使用序列的案例&#xff1a; 1. **为主键生成唯一值**&#xff1a; 在Oracle中&#xff0c;序列最常用的场景是…...

热点观察 | 《姜饼人王国》新作来袭、《Monopoly GO!》荣登5月全球畅销榜榜首

本周出海热点&#xff1a; 1. 中国品牌借欧洲杯打响知名度 2. 米哈游玩家切割二次元 3. 6月27日&#xff0c;Steam游戏《六月衷曲》上线TapTap 4. 《Monopoly GO!》荣登5月全球畅销榜榜首 5. 《地下城与勇士》拿下本周亚洲T1市场畅销榜冠军 6. 《姜饼人王国》新作强势登顶…...

智能网络构建:探索大模型在网络领域的应用

网络领域以其高度复杂性和快速迭代为特点&#xff0c;完成从网络设计、配置、诊断到安全的网络任务需要广泛的专业知识。这些任务的固有复杂性&#xff0c;加上网络技术和协议不断变化的格局&#xff0c;为传统基于机器学习的方法带来了显著的障碍。这些方法在泛化和自动化网络…...

C++编程逻辑讲解step by step:定义一个Person类,它的每个对象表示一个人。

题目 定义一个Person类,它的每个对象表示一个人。数据成员必须包含姓名、出生年份、死亡年份&#xff0c;一个构造函数&#xff0c;一析构函数&#xff0c;读取数据的成员函数&#xff0c;一个print()成员函数显示所有数据。 #include <iostream> using namespace std;…...

DBdoctor产品介绍

基本信息 DBdoctor是一款企业级数据库监控、巡检、性能诊断、SQL审核与优化平台&#xff0c;致力于解决一切数据库性能问题。采用eBPF技术可对数据库做细粒度的扫描&#xff0c;帮助您一分钟内找到数据库性能问题&#xff0c;实现性能诊断百倍提效。针对数据库性能诊断门槛高、…...

一加Ace3 刷机救砖简化说明

注意&#xff1a;工具使用英文目录&#xff0c;支持救砖和降级。PJE110国行版&#xff0c;CPH2609国际版。目前国行版不能完美转换国际版&#xff0c;每次升级都需要刷oplusstanvbk&#xff0c;不建议使用。跨国转换或ROOT一定先解锁Bootloader&#xff0c;可以使用“一加全能工…...

【服务器05】之【登录/注册账号成功转至游戏场景】

Unity登录注册数据库 打开【服务器01】的文章项目 导入新UI系统 点击2D 双击输入栏位置 修改输入框尺寸及位置 放大字体 修改默认输入文字 发现中文字变成了口口口口 原因是新UI系统不支持中文&#xff0c;解决这个问题需要更换字体 并且修改输入时字体大小 我们取电脑中找Fon…...

平价蓝牙耳机推荐性价比高,性价比高的蓝牙耳机学生党推荐

市场上的蓝牙耳机价格从几十元到几百甚至上千不等&#xff0c;性能与价格也呈现多样化&#xff0c;对于学生党来说&#xff0c;一个理想的选择是那些性价比高的平价蓝牙耳机&#xff0c;它们在不牺牲必要功能的同时&#xff0c;提供了可接受的音质和足够的便利性&#xff0c;接…...

【华为战报】5月、6月HCIP考试战报!

华为认证&#xff1a;HCIA-HCIP-HCIE 点击查看&#xff1a; 【华为战报】4月 HCIP考试战报&#xff01; 【华为战报】2月、3月HCIP考试战报&#xff01; 【华为战报】11月份HCIP考试战报&#xff01; 【HCIE喜报】HCIE备考2个月丝滑通关&#xff0c;考试心得分享&#xff…...

OBD诊断

文章目录 OBD 参考标准OBD 服务OBD服务中的DTCOBD服务中0x03和0x07的区别参考 OBD 参考标准 OBD的标准&#xff1a; ISO 15031 Road Vehicles-Communication between vehicle and external equipment for emission-related diagnostics OBD 服务 序号ID服务说明服务详解10x0…...

Elasticsearch 聚合查询

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~~ &#x1f4a5;&#x1f4a5;个人主页&#xff1a;奋斗的小羊 &#x1f4a5;&#x1f4a5;所属专栏&#xff1a;C语言 &#x1f680;本系列文章为个人学习…...

adb remount fails - mount: ‘system‘ not in /proc/mounts 解决办法

mount -o rw,remount /挂载根 mount -o ro,remount /将状态重置为“ro” 以下是我个人的一些话 我热衷于在网络上分享我遇到的问题和解决方案。如果你有任何问题或需要帮助&#xff0c;欢迎留言交流&#xff0c;在共同学习的道路上一起进步。我很高兴结识那些在学习上积极进取…...

百元蓝牙耳机推荐2024哪个好?蓝牙耳机性价比之王推荐

现在的百元价位的蓝牙耳机成为了许多消费者入门级的选择&#xff0c;它不仅需要满足基础的通话需求&#xff0c;更要在音质、舒适度、续航能力等多方面达到一定的标准&#xff0c;随着技术的发展和市场的竞争激烈&#xff0c;各大品牌在这一价格区间推出了极具竞争力的产品&…...

Spring项目报错解读与全部报错详解

你好,我是Qiuner. 为帮助别人少走弯路和记录自己编程学习过程而写博客 这是我的 github https://github.com/Qiuner ⭐️ ​ gitee https://gitee.com/Qiuner &#x1f339; 如果本篇文章帮到了你 不妨点个赞吧~ 我会很高兴的 &#x1f604; (^ ~ ^) 想看更多 那就点个关注吧 我…...

10秒教会你mysql的连接

连接MySQL数据库通常可以通过多种方法实现&#xff0c;以下是几种常见的方法&#xff0c;我将按照您的要求以清晰、分点的方式归纳说明&#xff1a; 1. 使用MySQL命令行客户端 打开终端或命令提示符&#xff1a;首先&#xff0c;打开您的计算机上的终端或命令提示符窗口。输入…...

万物皆可爬——亮数据代理IP+Python爬虫批量下载百度图片助力AI训练

&#x1f482; 个人网站:【 摸鱼游戏】【神级代码资源网站】【导航大全】&#x1f91f; 一站式轻松构建小程序、Web网站、移动应用&#xff1a;&#x1f449;注册地址&#x1f91f; 基于Web端打造的&#xff1a;&#x1f449;轻量化工具创作平台&#x1f485; 想寻找共同学习交…...

OpenCv形态学(一)

目录 形态学转换 结构元素 腐蚀 膨胀 开运算 闭运算 形态学梯度 顶帽 黑帽 图像轮廓 查找轮廓 绘制轮廓 形态学转换 形态变换是一些基于图像形状的简单操作。通常在二值图像上执行。它需要两个输入&#xff0c;一个是我们的原始图像&#xff0c;第二个是决定操作性…...

CSS基础汇总

CSS 1. 选择器 标签选择器 通过标签名找标签&#xff08;把指定的样式应用到某一个、组、类标签上&#xff09; id选择器 通过id属性值找标签&#xff0c;关键符号#id值{样式} 复合选择器 1、并列选择器&#xff1a;关键符号&#xff0c;用法&#xff1a;选择器1&#xff0c…...

cocos creator让所有button点击时播放音效

原理&#xff1a; 利用prototype属性&#xff0c;通过重写 cc.Button.prototype._onTouchEnded 方法&#xff0c;以便在按钮被点击时播放音频。通过重写其 _onTouchEnded 方法&#xff0c;可以添加自定义行为&#xff0c;如播放音频。 概念解释&#xff1a; prototype&#…...

mybatisplus自带的雪花算法(IdType.ASSIGN_ID)无法自动生成弊端缺点,以及改进方法

前言 今日在使用mybatisplus的雪花算法自动给id赋值时发现怎么都是null的情况&#xff0c;这尼玛测了半天&#xff0c;终于发现巨坑&#xff0c;废话不多说&#xff0c;直接上干货 IService.save 只有调用IService中的save方法才能正常生成id&#xff0c;像IService.saveBatc…...

单位转换:将kb转换为 MB ,GB等形式

写法一&#xff1a; function formatSizeUnits(kb) {let units [KB, MB, GB, TB, PB,EB,ZB,YB];let unitIndex 0;while (kb > 1024 && unitIndex < units.length - 1) {kb / 1024;unitIndex;}return ${kb.toFixed(2)} ${units[unitIndex]}; } console.log(for…...

优思学院|「按计划推动型」与「需求拉动型」的生产模式

针对生产架构做对比分类的用语&#xff0c;主要有按计划推进型与需求拉动型。 「按计划推动型」与「需求拉动型」两者乃是生产架构上常使用、成对比的两个用语。不过&#xff0c;有时不只用来指单纯的生产现场架构&#xff0c;也有人把它应用在更广泛的生产架构设计上。 按计划…...

解释什么是lambda函数?它有什么好处?

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…...

wordpress页面加载时间代码/网站关键词优化wang

最近我不得不在 Laravel 7 实现 通用唯一识别码 &#xff08; UUIDs &#xff09;&#xff0c;并遇到一些问题。我希望这帖子可为其他正在做相同事情的人解惑。 使用 UUIDs 的高级理由 A) 它们从你的 统一资源定位符 移除编号的 身份识别号 &#xff0c;故用户不能看到你的应…...

国内做的好网站有哪些/关键词推广seo怎么优化

按天备份 按周备份 mysql的mysqldump备份什么时候能派上用场1&#xff0c;迁移或者升级数据库时2&#xff0c;增加从库的时候3&#xff0c;如果因为硬件或特殊情况&#xff0c;主库或者从库宕机&#xff0c;主从可以互相切换&#xff0c;无需备份4&#xff0c;人为的DDL,DML语句…...

社区平安建设基层网站/上海seo培训中心

ES安装及head插件安装1.官网下载2.windows下安装3.如果内存小 修改配置文件jvm.options启动参数4.启动 双击elasticsearch.bat5.访问 127.0.0.1:92006.安装可视化界面 以及启动7.解决跨域问题8.再次启动elasticsearch-head-master 访问http://localhost:9100/总结JDK1.8 &#…...

微信上wordpress/seo就业

SVG剪裁路径和遮罩jQuery幻灯片是一款基于SVG clipPath和mask遮罩元素实现的网站动画幻灯片代码特效。在线演示本地下载转载于:https://www.cnblogs.com/wwhhq/p/8283759.html...

网站推广中h1标签的重要性/东莞seo优化案例

本文讲的是傻瓜&#xff0c;社区才是关键啊!&#xff0c;【编者的话】本文是Docker用户命名空间功能实现作者写的一篇关于开源社区的文章。他的观点是一切的成功都离不开社区的鼎力支持&#xff0c;所以当你加入一个开源项目的时候&#xff0c;尽量去真正的参与其中&#xff0c…...

网站建设经/长沙百度关键词推广

转&#xff1a;http://www.cnblogs.com/nbpowerboy/p/3355943.html 前段时间工作很忙&#xff0c;好久没更新博客了&#xff0c;趁国庆休假期间&#xff0c;整理了两个之前积累很实用的企业集成组件&#xff0c;并在真正的大型项目中经受住了考验&#xff1a;.Net版SAP RFC适配…...