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.pswp.cn/diannao/37405.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/37405.shtml
英文地址,請注明出處:http://en.pswp.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體系主要分為字節流和字符流兩大類…

java對word文檔轉圖片,轉PDF

話不多說&#xff0c;直接入題 先引包 <dependency><groupId>com.luhuiguo</groupId><artifactId>aspose-words</artifactId><version>23.1</version></dependency> word文檔轉圖片 import com.aspose.words.Document; impor…

防爆配電箱航空插頭正確安裝

防爆配電箱航空插頭的安裝確實有特殊要求&#xff0c;這些要求旨在確保配電箱在潛在危險環境中的安全運行。以下是一些關鍵的安裝要求&#xff1a; 安裝環境&#xff1a;防爆配電箱應安裝在危險區域之外的安全地點&#xff0c;遠離潛在的爆炸源和危險物質。安裝環境應保持干燥、…

springboot使用feign調用不依賴cloud

在使用spring boot調用第三方api中&#xff0c;常用的是okhttp、apache http client等&#xff0c;但是直接使用下來還是有點繁瑣&#xff0c;需要手動轉換實體。 在springcloud中有個openfeign調用&#xff0c;第一次體驗到調用接口還能這么絲滑。注解寫道接口上&#xff0c;…

17859劃分準則小結

17859《劃分準則》 發布時間&#xff1a;1999.9.13 實施時間&#xff1a;2001.1.1 計算機信息系統安全保護能力的五個等級&#xff1a; 第一級&#xff1a;用戶自主保護級 第二級…

數據結構簡介

在容器的基礎之上&#xff0c;java引入了數據結構的概念。數據結構可以簡單地理解成是一個以特定的布局方式來存儲數據的容器。但是我個人覺得這種理解方式不太合理&#xff0c;根據我們學的數據結構的內容&#xff0c;我更傾向于數據結構是數據在容器中的布局方式&#xff0c;…

rtthread stm32h743的使用(十一)spi設備使用

我們要在rtthread studio 開發環境中建立stm32h743xih6芯片的工程。我們使用一塊stm32h743及fpga的核心板完成相關實驗&#xff0c;核心板如圖&#xff1a; 1.建立新工程&#xff0c;選擇相應的芯片型號及debug引腳及調試器 2.編譯下載&#xff0c;可以看到串口打印正常 3.…

Python商務數據分析知識專欄(一)——Python編程基礎

Python商務數據分析知識專欄&#xff08;一&#xff09;——Python編程基礎 一、認識python二、編寫python程序三、認識python數據結構四、條件判斷及分支語句五、使用def定義函數六、認識面向對象七、讀取文件數據八、模塊和第三方庫專欄一&#xff08;Python基礎&#xff09;…

c++ 解決區間最大數和矩陣最大面積

給定一個實數序列&#xff0c;設計一個最有效的算法&#xff0c;找到一個總和數最大的區間等于某個事先給定的數字。 我們可以使用前綴和和哈希表來設計一個高效的算法。這個算法的時間復雜度是 O(n)&#xff0c;空間復雜度也是 O(n)。 #include <vector> #include <…

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…

Spring Boot中的熱部署配置

Spring Boot中的熱部署配置 大家好&#xff0c;我是免費搭建查券返利機器人省錢賺傭金就用微賺淘客系統3.0的小編&#xff0c;也是冬天不穿秋褲&#xff0c;天冷也要風度的程序猿&#xff01;今天我們將探討如何在Spring Boot項目中實現熱部署配置&#xff0c;提升開發效率和項…

C++實現Qt的信號+槽功能

在 Visual Studio (VS) 上使用 C 實現類似 Qt 的信號和槽機制是完全可能的&#xff0c;但 Qt 的信號和槽系統是基于其特定的元對象系統&#xff08;Meta-Object System, MOC&#xff09;的&#xff0c;這需要一些特定的預處理器和代碼生成步驟。 如果你不想使用 Qt&#xff0c;…

vue路由傳參和react 路由傳參

路由跳轉的方式 1、聲明式導航 <router-link to"導航的地址"> 2、編程式導航 編程式導航有三種方法來進行導航 router.push router.replace router.go params傳參和query傳參 1、 params 傳參(不在URL中顯示參數) 在父路由跳轉到子路由時&#xff0c;也可…

【Django】網上蛋糕項目商城-熱銷和新品

概念 本文將完成實現項目的熱銷和新品兩個分類的商品列表進行分頁展示。 熱銷和新品功能實現步驟 在head.html頭部頁面中點擊這兩個超鏈接向服務器發送請求。 在urls.py文件中定義該請求地址 path(goodsrecommend_list/,views.goodsrecommend_list) 在views.py文件中定義g…

JDBC中的批處理是什么?如何使用?

JDBC中的批處理是指將多個關聯的SQL語句組合成一個批處理&#xff0c;并將它們作為一個調用提交給數據庫。這種方法可以減少通信的資源消耗&#xff0c;從而提高性能。以下是關于JDBC批處理的具體使用和步驟&#xff1a; 1. JDBC批處理的基本概念 批處理定義&#xff1a;將多…