C++ const成员函数间接修改对象状态的合法性与优化方案咨询
问题1:现有代码是否存在未定义行为?
是,明确存在未定义行为。
根源出在:当你通过accounts()拿到的是const DonationsAccount&类型的账户实例,后续donate方法调用协会的donate接口时,协会会直接调用对应账户的非const方法put,相当于你隐式对const对象调用了非const成员函数,这在C++标准里属于明确的未定义行为,只是因为当前编译器实现没有对堆/栈上的const对象做内存页写保护,所以看起来能正常运行而已。
另外还有一个隐藏的问题:如果Association实例本身是const的,你把它的this指针赋值给非const的VirtualAssociation*成员本身就是非法的,编译就会报错,现有代码能跑只是因为你测试用的association是non-const的。
问题2:将_money声明为mutable能不能解决问题?
首先明确mutable的作用:它修饰的类成员,不受const语义限制——哪怕是在const成员函数中,或者对象本身是const的,该成员也可以被修改,是C++为了区分「逻辑const」和「物理const」提供的语法。
但回到你的代码:你现在的未定义行为和_money本身是不是可修改没关系,根源是你在const对象上调用了非const的put方法,所以单纯给_money加mutable解决不了问题。
如果你同时把put方法也改成const的,那未定义行为就会消失,行为是明确定义的:此时put作为const成员函数,可以修改mutable的_money,符合标准要求。但这种方案本质上是破坏了DonationsAccount本身的const语义,只是能用,但不够优雅。
问题3:更优的实现方案
结合你补充的图场景需求,最优的方案是引入代理层,不要直接返回真实的节点/账户对象的引用,而是返回一个只开放你允许的操作的代理类:
- 真实的账户/节点类只在
Association/图内部使用,不对外暴露 - 对外暴露的是
AccountHandle/NodeHandle之类的代理类,代理类只持有图的指针和对象索引,只开放你允许调用的方法(比如donate/add_edge),不开放赋值、移动、修改内部状态的接口 - 代理类的所有方法都直接转发给持有它的图/协会对象去执行,所有合法性检查、副作用处理都在图内部统一完成
简化的代码示例如下:
class Association; class AccountHandle { friend class Association; private: Association* _assoc; size_t _idx; AccountHandle(Association* assoc, size_t idx) : _assoc(assoc), _idx(idx) {} public: // 只开放你允许的方法 void donate(int amount); int balance() const; // 禁止用户自行拷贝、赋值、构造代理对象 AccountHandle(const AccountHandle&) = delete; AccountHandle& operator=(const AccountHandle&) = delete; }; class Association { public: Association() { _accounts.push_back(0); _handles.emplace_back(this, 0); } void donate(size_t idx, int amount) { // 可以在这里加范围检查、权限校验等逻辑 _accounts[idx] += amount; } int get_balance(size_t idx) const { return _accounts[idx]; } // 返回代理对象列表,而不是真实账户 const std::vector<AccountHandle>& accounts() const { return _handles; } private: std::vector<int> _accounts; std::vector<AccountHandle> _handles; }; // 代理方法的实现 void AccountHandle::donate(int amount) { _assoc->donate(_idx, amount); } int AccountHandle::balance() const { return _assoc->get_balance(_idx); }
这种方案完全符合const正确性:代理本身是不可修改的(用户不能改代理的指向、不能赋值),但可以通过代理调用你允许的、会修改图状态的操作,所有操作都受控,不会出现用户意外修改内部状态的问题,也完全没有未定义行为。
内容的提问来源于stack exchange,提问作者Maximilian Keßler

