C++实现简化版Qt信号槽机制(2):增加内存安全保障

在上一篇文章中《C++实现一个简单的Qt信号槽机制》,我们基于前面的反射代码实现了信号槽的功能。
但是上一篇的代码中没有对象生命周期管理的机制,如果在对象的生命周期结束后还存在未断开的信号和槽连接,那么信号触发时可能会尝试访问已经被析构的对象,从而引发内存访问异常。这个设计缺陷在C++越来越

方案的确定

在Qt框架中,QObject的析构函数,在对象销毁时会自动断开所有信号和槽的连接。从而避免出现这种情况。
笔者考虑了一下,没有采取这种方案,因为:

  • 这会要求信号和槽两个对象互相存储双方的连接情况,在一定程度上浪费了内存。
  • 另一方面,我们还没有支持对象的内存管理,没有弱引用机制。如果通过析构函数反连接的做法那么还需要单独再次引入弱引用机制。

经过一轮思索,笔者选择的方案是:

  • 引入弱引用机制,解决内存管理问题
  • 在信号触发时,自动跳过已经析构的槽对象,并清理无效的槽链接。

实现

弱引用机制

C++标准库本身支持一套内存管理机制,那就是std::shared_ptr体系。这套体系也支持弱指针引用,我们尽可能避免重复造轮子,因此我们直接采用这种机制。
综合考虑下来,我们决定让我们的的IReflectable 从std::enable_shared_from_this派生。(之所以不是QObject从shared_ptr派生,是因为IReflectable 有方法工厂,我们希望方法工厂返回的IReflectable 实例就是被shared_ptr承载的)

class IReflectable : public std::enable_shared_from_this<IReflectable> {//...}

然后,在信号触发的时候,判断对象是否有效:

		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) {auto ptr = std::get<0>(slot_info).lock();// 获取弱引用的有效指针if (ptr) {ptr->invoke_member_func_by_name(std::get<1>(slot_info).c_str(), std::forward<Args>(args)...);}// else 对象已经析构了}}else {assert(false);}}

之所以没有在else那里清理掉已经无效的链接,是因为在信号槽连接的时候,外部可能保存了数组的迭代器:
[外链图片转存中…(img-jIOqrBqD-1719542815307)]
直接返回迭代器的好处是在disconnect的时候性能特别好,并且实现简单:

template <typename T>bool disconnect(T connection) {//T是个这个类型:std::make_optional(std::make_tuple(this, itMap, it));//由于T过于复杂,就直接用模板算了if (!connection) {return false;}auto& tuple = connection.value();if (std::get<0>(tuple) != this) {return false;//不是我的connection呀}std::get<1>(tuple)->second.erase(std::get<2>(tuple));return true;}

缺点是连接的信号一旦移除之后其他迭代器会失效。这样做肯定不行。于是把迭代器的数组类型从vector换成了list。避免迭代器失效,同时保留了connect返回迭代器的优势,同时支持在信号触发的时候直接清理掉已经析构的槽对象,避免死对象无法清除。一举N得。
于是只需要修改这一行即可:

using connections_list_type = std::vector<std::tuple< std::weak_ptr<QObject>, std::string>>;

改为:

using connections_list_type = std::list<std::tuple< std::weak_ptr<QObject>, std::string>>;

其他地方都不用修改,这就是C++ auto 的力量。

接下来,把前面raw_emit_signal_impl函数中对象无效后从list清理的逻辑补上,大功告成。
实现的时候,考虑性能问题,只在发现存在无效对象时才进行批量移除,因此引入局部变量has_invalid_slot 来标识这种情况。代码如下:

		template<typename... Args>void raw_emit_signal_impl(const char* signal_name, Args&&... args) {auto it = connections.find(signal_name);if (it != connections.end()) {auto& slots = it->second; // 获取槽信息列表的引用bool has_invalid_slot = false;for (const auto& slot_info : slots) {auto ptr = std::get<0>(slot_info).lock(); // 锁定弱引用if (ptr) {ptr->invoke_member_func_by_name(std::get<1>(slot_info).c_str(), std::forward<Args>(args)...);}else {has_invalid_slot = true;}}if (has_invalid_slot) {//如果存在无效对象,则执行一轮移除操作auto remove_it = std::remove_if(slots.begin(), slots.end(),[](const auto& slot_info) {return std::get<0>(slot_info).expired(); // 检查弱引用是否失效});slots.erase(remove_it, slots.end());}}else {/*没找到这个信号,要不要assert?*/ }}

解决要求所有的QObject派生对象都需要通过make_shared

由于使用了C++的shared_ptr,因此我们的QObject派生对象都需要通过make_shared创建,否则成员函数make_weeked和make_shared都是无效的。
为了避免使用者用错,我们connect的时候,会对weak_ptr是否有效进行检查,这样就能确保我们的内存管理机制符合预期,规避开发者误用了。
在这里插入图片描述

那么,我们一系列的函数接口都要进行优化,使其与share_ptr类型更好的适配,避免开发者大量编写ptr.get()等干扰可读性的代码。
我们以connect函数为例,提供两个重载,以直接支持shared_ptr:

template <typename SlotClass>
auto connect(const char* signal_name, std::shared_ptr<SlotClass> slot_instance, const char* slot_member_func_name) {return connect(signal_name, slot_instance.get(), slot_member_func_name);
}
template <typename SignalClass, typename SignalType, typename SlotClass, typename SlotType>
auto connect(SignalType SignalClass::* signal, std::shared_ptr<SlotClass> &slot_instance, SlotType SlotClass::* slot) {return connect(signal, slot_instance.get(), slot);
}

这样,使用者就可以保持代码的简洁性了。

不过,在适配其他函数的时候,笔者还是有点纠结:

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());
}

改为

template <typename T, size_t N = 0>
std::any get_field_value(T* obj, const char* name) {return obj ? __get_field_value_impl(*obj, name, T::properties()) : std::any();
}

就需要对obj进行判空。因为指针的语义是可空,而引用的语义是非空,语义宽松了,也增加了一种新的错误类型(第一种错误是找不到,新增了对象为空的错误),新引入了一个新的错误这个点确实让我有点纠结,但又没有什么好办法,只能说鱼和熊掌不能兼得呀。

最终,测试代码里MyStruct obj;都变成make_shared即可:auto obj = std::make_shared();

这一轮优化后,完整代码如下:

#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 obj ? __get_field_value_impl(*obj, name, T::properties()) : std::any();}// 使用 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 obj ? __assign_field_value_impl(*obj, name, value, T::properties()) : std::any();}// 成员函数调用相关: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 obj ? __invoke_member_func_impl(obj, name, funcs, std::forward<Args>(args)...) : std::any();}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 obj ? __invoke_member_func_impl<true>(obj, name, funcs, std::forward<Args>(args)...) : std::any();}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 std::enable_shared_from_this<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::shared_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::shared_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::shared_ptr<::refl::dynamic::IReflectable> create_instance() { return std::make_shared<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 REFLEC_IMPL_SIGNAL(...) raw_emit_signal_impl(__func__ , __VA_ARGS__)class QObject :public refl::dynamic::IReflectable {private:// 信号与槽的映射,键是信号名称,值是一组槽函数的信息using connections_list_type = std::list<std::tuple< std::weak_ptr<IReflectable>, std::string>>;using connections_type = std::unordered_map<std::string, connections_list_type>;connections_type connections;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()) {auto& slots = it->second; // 获取槽信息列表的引用bool has_invalid_slot = false;for (const auto& slot_info : slots) {auto ptr = std::get<0>(slot_info).lock(); // 锁定弱引用if (ptr) {ptr->invoke_member_func_by_name(std::get<1>(slot_info).c_str(), std::forward<Args>(args)...);}else {has_invalid_slot = true;}}if (has_invalid_slot) {//如果存在无效对象,则执行一轮移除操作auto remove_it = std::remove_if(slots.begin(), slots.end(),[](const auto& slot_info) {return std::get<0>(slot_info).expired(); // 检查弱引用是否失效});slots.erase(remove_it, slots.end());}}else {/*没找到这个信号,要不要assert?*/ }}auto connect(const char* signal_name, refl::QObject* slot_instance, const char* slot_member_func_name) {if (!slot_instance || !signal_name || !slot_member_func_name) {throw std::runtime_error("param is null!");}assert(slot_instance->weak_from_this().lock());//target必须通过make_share构造!!因为要弱引用它std::string str_signal_name(signal_name);auto itMap = connections.find(str_signal_name);if (itMap != connections.end()) {itMap->second.emplace_back(slot_instance->weak_from_this(), slot_member_func_name);//必须插入末尾,因为返回了--end()迭代器指示这个链接return std::make_optional(std::make_tuple(this, itMap, --itMap->second.end()));}else {// 如果没找到,插入新元素到map中,并获取迭代器auto emplace_result = connections.emplace(std::make_pair(std::move(str_signal_name), connections_list_type()));itMap = emplace_result.first;itMap->second.emplace_back(slot_instance->weak_from_this(), slot_member_func_name);return std::make_optional(std::make_tuple(this, itMap, --itMap->second.end()));}}template <typename SlotClass>auto connect(const char* signal_name, std::shared_ptr<SlotClass> slot_instance, const char* slot_member_func_name) {return connect(signal_name, slot_instance.get(), slot_member_func_name);}template <typename SignalClass, typename SignalType, typename SlotClass, typename SlotType>auto 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);}throw std::runtime_error("signal name or slot_name is not found!");}template <typename SignalClass, typename SignalType, typename SlotClass, typename SlotType>auto connect(SignalType SignalClass::* signal, std::shared_ptr<SlotClass>& slot_instance, SlotType SlotClass::* slot) {return connect(signal, slot_instance.get(), slot);}template <typename T>bool disconnect(T connection) {//T是个这个类型:std::make_optional(std::make_tuple(this, itMap, it)); 由于T过于复杂,就直接用模板算了if (!connection) {return false;}auto& tuple = connection.value();if (std::get<0>(tuple) != this) {return false;//不是我的connection呀}std::get<1>(tuple)->second.erase(std::get<2>(tuple));return true;}};}// 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) {REFLEC_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() {auto obj = std::make_shared<MyStruct>();// # 静态反射部分:// 打印所有字段名称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.get(), [](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.get(), "x");std::cout << "Field x has value: " << std::any_cast<int>(x_value) << std::endl;auto y_value = refl::get_field_value(obj.get(), "y");std::cout << "Field y has value: " << std::any_cast<double>(y_value) << std::endl;//修改值:refl::assign_field_value(obj.get(), "y", 33.33f);y_value = refl::get_field_value(obj.get(), "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.get(), "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.get(), "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;auto obj1 = std::make_shared<MyStruct>();auto obj2 = std::make_shared<MyStruct>();// 连接obj1的信号到obj2的槽函数auto connection_id = obj1->connect("x_value_modified", obj2.get(), "on_x_value_modified");if (!connection_id) {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) {std::cout << "Signal connected to slot." << std::endl;}obj1->x_value_modified(666);// 触发信号return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/37405.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ValidateAntiForgeryToken、AntiForgeryToken 防止CSRF(跨网站请求伪造)

用途&#xff1a;防止CSRF&#xff08;跨网站请求伪造&#xff09;。 用法&#xff1a;在View->Form表单中: aspx&#xff1a;<%:Html.AntiForgeryToken()%> razor&#xff1a;Html.AntiForgeryToken() 在Controller->Action动作上&#xff1a;[ValidateAntiForge…

Java的IO体系

目录 1、Java的IO体系2、IO的常用方法3、Java中为什么要分为字节流和字符流4、File和RandomAccessFile5、Java对象的序列化和反序列化6、缓冲流7、Java 的IO流中涉及哪些设计模式 1、Java的IO体系 IO 即为 input 输入 和 output输出 Java的IO体系主要分为字节流和字符流两大类…

防爆配电箱航空插头正确安装

防爆配电箱航空插头的安装确实有特殊要求&#xff0c;这些要求旨在确保配电箱在潜在危险环境中的安全运行。以下是一些关键的安装要求&#xff1a; 安装环境&#xff1a;防爆配电箱应安装在危险区域之外的安全地点&#xff0c;远离潜在的爆炸源和危险物质。安装环境应保持干燥、…

17859划分准则小结

17859《划分准则》 发布时间&#xff1a;1999.9.13 实施时间&#xff1a;2001.1.1 计算机信息系统安全保护能力的五个等级&#xff1a; 第一级&#xff1a;用户自主保护级 第二级…

rtthread stm32h743的使用(十一)spi设备使用

我们要在rtthread studio 开发环境中建立stm32h743xih6芯片的工程。我们使用一块stm32h743及fpga的核心板完成相关实验&#xff0c;核心板如图&#xff1a; 1.建立新工程&#xff0c;选择相应的芯片型号及debug引脚及调试器 2.编译下载&#xff0c;可以看到串口打印正常 3.…

python查找支撑数 青少年编程电子学会python编程等级考试三级真题解析2022年3月

目录 python查找支撑数 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序代码 四、程序说明 五、运行结果 六、考点分析 七、 推荐资料 1、蓝桥杯比赛 2、考级资料 3、其它资料 python查找支撑数 2022年3月 python编程等级考试级编程题 一、题目要求…

RabbitMQ 的经典问题

文章目录 前言一、防止消息丢失1.1 ConfirmCallback/ReturnCallback1.2 持久化1.3 消费者确认消息 二、防止重复消费三、处理消息堆积四、有序消费消息五、实现延时队列六、小结推荐阅读 前言 当设计和运维消息队列系统时&#xff0c;如 RabbitMQ&#xff0c;有几个关键问题需…

第100+13步 ChatGPT学习:R实现决策树分类

基于R 4.2.2版本演示 一、写在前面 有不少大佬问做机器学习分类能不能用R语言&#xff0c;不想学Python咯。 答曰&#xff1a;可&#xff01;用GPT或者Kimi转一下就得了呗。 加上最近也没啥内容写了&#xff0c;就帮各位搬运一下吧。 二、R代码实现决策树分类 &#xff08;…

【漏洞复现】宏景HCM人力资源信息管理系统——任意文件读取漏洞

声明&#xff1a;本文档或演示材料仅供教育和教学目的使用&#xff0c;任何个人或组织使用本文档中的信息进行非法活动&#xff0c;均与本文档的作者或发布者无关。 文章目录 漏洞描述漏洞复现测试工具 漏洞描述 宏景HCM人力资源信息管理系统是一款全面覆盖人力资源管理各模块…

docker pull 镜像的时候遇到Pulling fs layer问题

最近遇到一个很奇怪的问题,docker pull 镜像的时候,总是出现Pulling fs layer问题,导致镜像拉取不成功,以前是安装好docker,正常拉取镜像都是没什么问题的,在这里记录一下这个问题的解决方法,当然,可能并不通用。 1、进入阿里云容器服务 地址:https://cr.console.aliy…

【Django】网上蛋糕项目商城-热销和新品

概念 本文将完成实现项目的热销和新品两个分类的商品列表进行分页展示。 热销和新品功能实现步骤 在head.html头部页面中点击这两个超链接向服务器发送请求。 在urls.py文件中定义该请求地址 path(goodsrecommend_list/,views.goodsrecommend_list) 在views.py文件中定义g…

AV Foundation学习笔记二 - 播放器

ASSets AVFoundation框架的最核心的类是AVAsset&#xff0c;该类是整个AVFoundation框架设计的中心。AVAsset是一个抽象的&#xff08;意味着你不能调用AVAsset的alloc或者new方法来创建一个AVAsset实例对象&#xff0c;而是通过该类的静态方法来创建实例对象&#xff09;、不…

DevOps CMDB平台整合Jira工单

背景 在DevOps CMDB平台建设的过程中&#xff0c;我们可以很容易的将业务应用所涉及的云资源&#xff08;WAF、K8S、虚拟机等&#xff09;、CICD工具链&#xff08;Jenkins、ArgoCD&#xff09;、监控、日志等一次性的维护到CMDB平台&#xff0c;但随着时间的推移&#xff0c;…

PHP爬虫类的并发与多线程处理技巧

PHP爬虫类的并发与多线程处理技巧 引言&#xff1a; 随着互联网的快速发展&#xff0c;大量的数据信息存储在各种网站上&#xff0c;获取这些数据已经成为很多业务场景下的需求。而爬虫作为一种自动化获取网络信息的工具&#xff0c;被广泛应用于数据采集、搜索引擎、舆情分析…

关于组织赴俄罗斯(莫斯科)第 28 届国际汽车零部件、汽车维修设备和商品展览会商务考察的通知

关于组织赴俄罗斯&#xff08;莫斯科&#xff09; 第 28 届国际汽车零部件、汽车维修设备和商品展览会商务考察的通知 展会名称&#xff1a;俄罗斯&#xff08;莫斯科&#xff09;第 28 届国际汽车零部件、汽车零部件、汽车维修设备和商品展览会 时间&#xff1a;2024 年 8 月…

Python | Leetcode Python题解之第204题计数质数

题目&#xff1a; 题解&#xff1a; MX5000000 is_prime [1] * MX is_prime[0]is_prime[1]0 for i in range(2, MX):if is_prime[i]:for j in range(i * i, MX, i):#循环每次增加iis_prime[j] 0 class Solution:def countPrimes(self, n: int) -> int:return sum(is_prim…

【MongoDB】分布式数据库入门级学习

SueWakeup 个人主页&#xff1a;SueWakeup 系列专栏&#xff1a;为祖国的科技进步添砖Java 个性签名&#xff1a;保留赤子之心也许是种幸运吧 本文封面由 凯楠&#x1f4f8;友情提供 凯楠&#x1f4f8; - 不夜长安 目录 MongoDB 相关 数据库排行榜单 MongoDB 中文官网 菜鸟…

如何把mkv转成mp4?介绍一下将mkv转成MP4的几种方法

如何把mkv转成mp4&#xff1f;如果你有一个MKV格式的视频文件&#xff0c;但是需要将其转换为MP4格式以便更广泛地在各种设备和平台上播放和共享&#xff0c;你可以通过进行简单的文件格式转换来实现。转换MKV到MP4格式可以提供更好的兼容性&#xff0c;并确保你的视频文件能够…

在预训练语言模型主流架构

文章目录 编码器-解码器架构因果解码器架构前缀解码器架构在预训练语言模型时代,自然语言处理领域广泛采用了预训练 + 微调的范式,并诞生了以 BERT 为代表的编码器(Encoder-only)架构、以 GPT 为代表的解码器(Decoder-only)架构和以 T5 为代表的编码器-解码器(Encoder-d…

React:tabs或标签页自定义右击菜单内容,支持内嵌iframe关闭菜单方案

React&#xff1a;tabs或标签页自定义右击菜单内容&#xff0c;支持内嵌iframe关闭菜单方案 不管是react、vue还是原生js&#xff0c;原理是一样的。 注意如果内嵌iframe情况下&#xff0c;iframe无法使用事件监听&#xff0c;但是可以使用iframe的任何点击行为都会往父级wind…