修复对象初始化问题以确保调用重写的notifyChange方法
这个问题其实是C++对象构造阶段的经典虚函数行为坑:当你在基类Watcher的构造函数里调用m_watched->addWatcher(this),而addWatcher又触发了watcher->notifyChange()时,此时Property对象还没完成初始化——子类的成员和虚表都还没准备好,所以虚函数调用会绑定到基类Watcher的版本,而不是你预期的Property重写版本。
下面提供几个符合现代C++规范(无需修改m_watched访问权限)的解决思路:
1. 分离构造与注册逻辑(最直观的方案)
把自动注册的逻辑从构造函数中抽出来,改成手动调用的方法,确保在对象完全构造后再触发虚函数调用:
template<typename T> class Watcher { public: T* m_watched { nullptr }; Watcher() = default; // 构造函数仅完成成员初始化,不做副作用操作 explicit Watcher(T* watched) : m_watched(watched) {}; // 新增手动注册方法 void registerWithWatched() { if (m_watched) { m_watched->addWatcher(this); } } virtual void notifyChange(int = 0) {std::cout << "Watcher::notifyChange()\n";}; }; // Property类保持原有逻辑即可 class Property : public Watcher<Config> { public: explicit Property(Config* config) : Watcher<Config>(config) {}; void notifyChange(int = 0) override { std::cout << "Property::notifyChange()\n"; } };
使用方式:
Config myConfig; Property myProp(&myConfig); // 此时myProp已经完全构造,调用注册会触发子类的notifyChange myProp.registerWithWatched();
优点:逻辑清晰,用户完全控制注册时机,符合现代C++“构造仅做初始化”的设计原则;缺点:需要用户手动调用注册方法,有忘记调用的风险。
2. 使用工厂函数封装创建与注册(更安全的封装方案)
把对象的创建和注册逻辑封装到工厂函数里,强制用户通过工厂函数创建对象,确保注册操作在对象完全构造后执行:
template<typename T> class Watcher { public: T* m_watched { nullptr }; Watcher() = default; explicit Watcher(T* watched) : m_watched(watched) {}; virtual void notifyChange(int = 0) {std::cout << "Watcher::notifyChange()\n";}; }; class Property : public Watcher<Config> { public: // 把构造函数设为私有,强制通过工厂函数创建 explicit Property(Config* config) : Watcher<Config>(config) {}; // 工厂函数负责创建+注册 static std::unique_ptr<Property> create(Config* config) { auto prop = std::make_unique<Property>(config); // 此时prop指向的对象已经完全构造,虚表是子类的 prop->m_watched->addWatcher(prop.get()); return prop; } void notifyChange(int = 0) override { std::cout << "Property::notifyChange()\n"; } };
使用方式:
Config myConfig; auto myProp = Property::create(&myConfig);
优点:封装性强,用户无需关心注册细节,从根源上避免了构造期间的虚函数问题;缺点:需要使用智能指针(或返回对象并利用移动语义),稍微增加了一点复杂度。
3. 延迟notifyChange调用(适合批量场景)
如果你的业务场景允许延迟通知,可以修改Watchable的逻辑,不在addWatcher时立即调用notifyChange,而是提供一个统一的触发方法,在所有对象都构造完成后再批量通知:
template<typename T> class Watchable { public: std::vector<Watcher<T>*> m_watchers; void addWatcher(Watcher<T>* watcher) { m_watchers.push_back(watcher); // 移除立即调用,改为后续手动触发 // watcher->notifyChange(); } // 新增批量通知方法 void notifyAllWatchers(int param = 0) { for (auto watcher : m_watchers) { watcher->notifyChange(param); } } };
使用方式:
Config myConfig; Property myProp(&myConfig); // 此时不会触发notifyChange myConfig.notifyAllWatchers(); // 此时myProp已构造完成,会调用子类版本
优点:适合需要批量添加观察者后统一通知的场景;缺点:改变了原有“添加即通知”的行为,需要业务逻辑适配。
核心思路总结:C构造期间的虚函数绑定是静态的(绑定到当前正在构造的基类),所以要避免在基类构造函数中触发依赖子类虚函数的操作。现代C更倾向于分离初始化与副作用操作,或者通过工厂函数确保对象状态完整后再执行后续逻辑。
内容的提问来源于stack exchange,提问作者blackcat

