回滚的本质是状态快照而非操作日志;应在事务开始前对关键状态做深拷贝或字段级轻量备份,用RAII自动管理生命周期,并支持嵌套与零拷贝等优化策略。

回滚的本质是状态快照,不是操作日志
事务回滚在 C++ 里最容易踩的坑,就是试图记录“做了什么”,然后反向执行——比如 push_back 后记个 pop_back,set_value(42) 后记个 set_value(old)。这在并发、异常中途退出、对象析构顺序不确定时极难保证正确性。真正可控的方式,是**在事务开始前对关键状态做深拷贝(或等价的轻量备份)**,回滚时直接替换回来。
适用场景:配置管理器、游戏状态机、嵌入式设备寄存器模拟、单元测试中的可重置上下文。
- 只备份被事务修改的字段,而非整个对象(用
std::optional标记“是否被修改过”) - 避免在备份过程中触发用户自定义拷贝构造函数副作用(例如日志、网络调用),必要时用
memcpy或std::bit_cast(C++20)做 POD 类型的位级快照 - 若状态含裸指针或句柄(如
FILE*,int fd),不能直接拷贝,需抽象为 RAII 句柄并实现资源引用计数或事务性封装
用 RAII 封装事务生命周期,自动触发回滚
靠手动调用 commit() / rollback() 极易遗漏——尤其在异常路径中。C++ 的确定性析构才是天然的事务守门人。
class Transaction {
StateBackup backup_;
bool committed_ = false;
public:
explicit Transaction(State& s) : backup_(s) {}
~Transaction() { if (!committed_) rollback(); }
void commit() { committed_ = true; }
void rollback() { backup_.restore_to(state_); }
};
注意:backup_ 必须在 Transaction 对象内完成所有拷贝工作,不能延迟到 rollback() 才读取原始状态(此时原始对象可能已被部分修改或析构)。
- 构造函数中调用
backup_.capture(state),确保快照原子性 - 禁止将
Transaction对象绑定到临时量(如auto&& t = Transaction(s);),否则析构时机不可控 - 若需嵌套事务,需在
backup_中支持“叠加层”设计(例如保存上一层备份指针),而非简单覆盖
std::variant + std::optional 实现字段级差异备份
全量拷贝开销大?又不想手写每个字段的备份逻辑?用类型擦除加标记即可兼顾通用性与效率。
核心思路:每个可变字段用 std::variant<:monostate t backup>> 表示其当前状态 —— std::monostate 表示未修改,T 表示当前值,Backup 是备份值容器(含原始值+是否有效标志)。
template<typename t>
struct Backup {
T value;
bool valid = false;
void capture(const T& v) { value = v; valid = true; }
void restore(T& target) const { if (valid) target = value; }
};</typename>
实际使用时,状态类内部不直接暴露 int counter,而是用 std::variant<:monostate int backup>> counter_;写操作先检查是否首次修改,若是,则从真实对象中捕获当前值并存入 Backup。
- 避免为每个字段生成冗余的
Backup实例(未修改时不分配) - 恢复时仅对
std::holds_alternative的字段调用>(field) restore() - 该模式天然兼容 move-only 类型(如
std::unique_ptr),只要T可拷贝或可移动
std::shared_ptr 做不可变状态快照,零拷贝回滚
如果状态本身是只读的、且更新频率不高(如配置表、地图数据),更高效的做法是让状态对象始终不可变,事务只切换指针。
事务开始时,用 std::make_shared 创建快照;修改时构造新 State 对象,最后原子交换 std::atomic<:shared_ptr state>>。回滚只需把指针重新指向原快照。
- 无需深拷贝,无内存碎片风险,适合大结构体(如含
std::vector的二进制块) - 必须确保所有访问都通过 const 引用或指针,杜绝意外修改
- 若存在多线程并发读写,需配合
std::atomic<:shared_ptr>或读写锁,但回滚本身是 O(1) 指针赋值
复杂点在于状态变更的语义一致性——比如你改了 player.health 和 player.status,这两个字段必须来自同一个快照版本,否则会破坏业务约束。这要求事务粒度要对齐领域边界,不能只按内存布局切分。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











