Created
March 5, 2026 13:32
-
-
Save XDflight/f59520a0881d54741ae644b4a6b2225a to your computer and use it in GitHub Desktop.
A transactional C++ variable assignment manager with rollback/commit semantics.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * @file phantom_assignments.h | |
| * @author Qirui (Ridge) Da <qda@andrew.cmu.edu> | |
| * | |
| * @brief A transactional assignment manager with rollback/commit semantics. | |
| * | |
| * This library provides a way to perform "phantom" (tentative/reversible) assignments | |
| * to variables. All changes can be either committed (finalized) or reset (rolled back) | |
| * to their original values. This is useful for implementing undo functionality, | |
| * transactional operations, or speculative execution patterns. | |
| * | |
| * @section usage Basic Usage | |
| * | |
| * @code | |
| * // Create a PhantomAssignments manager for int and std::string types. | |
| * // AutoReset=true means destructor will automatically rollback uncommitted changes. | |
| * PhantomAssignments<true, int, std::string> pa; | |
| * | |
| * int x = 10; | |
| * std::string s = "hello"; | |
| * | |
| * // Make phantom assignments (original values are automatically backed up) | |
| * pa.assign(x, 20); // x is now 20, but original 10 is saved | |
| * pa.assign(s, "world"); // s is now "world", but "hello" is saved | |
| * | |
| * // Option 1: Rollback all changes | |
| * pa.reset(); // x is restored to 10, s is restored to "hello" | |
| * | |
| * // Option 2: Commit all changes (discard backup, keep current values) | |
| * pa.commit(); // Changes are finalized, backups are discarded | |
| * @endcode | |
| * | |
| * @section checkpoint Checkpoint-based Partial Rollback | |
| * | |
| * @code | |
| * PhantomAssignments<false, int> pa; | |
| * int a = 1, b = 2, c = 3; | |
| * | |
| * pa.assign(a, 10); | |
| * auto ckpt = pa.current(); // Save checkpoint after assigning 'a' | |
| * pa.assign(b, 20); | |
| * pa.assign(c, 30); | |
| * | |
| * pa.reset(ckpt); // Only 'b' and 'c' are rolled back; 'a' remains 10 | |
| * @endcode | |
| * | |
| * @section hooks Hooks for Custom Cleanup | |
| * | |
| * @code | |
| * PhantomAssignments<true, int> pa; | |
| * | |
| * // Register a hook that runs on reset (rollback) | |
| * pa.hook<true>([]{ std::cout << "Rolled back!\n"; }); | |
| * | |
| * // Register a hook that runs on commit | |
| * pa.hook<false>([]{ std::cout << "Committed!\n"; }); | |
| * @endcode | |
| * | |
| * @section auto_reset AutoReset Behavior | |
| * | |
| * - `PhantomAssignments<true, ...>`: Destructor calls `reset()` - changes are | |
| * automatically rolled back if not explicitly committed. | |
| * - `PhantomAssignments<false, ...>`: Destructor calls `commit()` - changes are | |
| * automatically committed if not explicitly reset. | |
| * | |
| * @section thread_safety Thread Safety | |
| * | |
| * All public methods are thread-safe (protected by internal mutex). | |
| * | |
| * @section variant_support Variant Support | |
| * | |
| * You can also instantiate with std::variant to flatten types: | |
| * @code | |
| * using MyVariant = std::variant<int, float>; | |
| * PhantomAssignments<true, MyVariant, std::string> pa; | |
| * // Equivalent to: PhantomAssignments<true, int, float, std::string> | |
| * @endcode | |
| */ | |
| #pragma once | |
| #include <stdexcept> | |
| #include <variant> | |
| #include <functional> | |
| #include <mutex> | |
| #include <forward_list> | |
| /** | |
| * @brief A transactional assignment manager that supports rollback and commit. | |
| * | |
| * @tparam AutoReset_ If true, destructor automatically resets (rolls back) all | |
| * uncommitted changes. If false, destructor automatically commits. | |
| * @tparam Types_ The types of variables that can be managed by this instance. | |
| * Only variables of these types can be passed to assign() or backup(). | |
| */ | |
| template <bool AutoReset_, class... Types_> | |
| class PhantomAssignments { | |
| public: | |
| static_assert(sizeof...(Types_) > 0, | |
| "PhantomAssignments must be instantiated with at least one type."); | |
| /** | |
| * @brief Type trait to check if a type T is one of the managed types. | |
| */ | |
| template <class T> | |
| using type_is_legal = std::disjunction<std::is_same<T, Types_>...>; | |
| /** | |
| * @brief Convenience variable template for type_is_legal. | |
| */ | |
| template <class T> | |
| bool constexpr inline static type_is_legal_v = type_is_legal<T>::value; | |
| /** | |
| * @brief The AutoReset policy of this instance. | |
| */ | |
| bool constexpr inline static AutoReset = AutoReset_; | |
| /** | |
| * @brief A variant type that can hold any of the managed types. | |
| */ | |
| using Types = std::variant<Types_...>; | |
| /** | |
| * @brief Function type for hooks (cleanup callbacks). | |
| */ | |
| using Destructor = std::function<void()>; | |
| /** | |
| * @brief Exception thrown when a precondition fails in assign(). | |
| * | |
| * This is thrown when assign() is called with `precondition = false`. | |
| */ | |
| struct precondition_failed : std::logic_error { | |
| precondition_failed() : std::logic_error( | |
| "Precondition failed for PhantomAssignments::assign") {} | |
| }; | |
| private: | |
| // ===== Implementation details (not part of public API) ===== | |
| template <class... Fs> | |
| struct overloaded : Fs... { using Fs::operator()...; }; | |
| template <class... Fs> overloaded(Fs...) -> overloaded<Fs...>; | |
| using AssignRecord = std::variant<std::pair< | |
| std::reference_wrapper<Types_>, Types_>...>; | |
| using HookRecord = std::pair<Destructor, bool>; | |
| using Record = std::variant<AssignRecord, HookRecord>; | |
| std::mutex mtx; | |
| std::forward_list<Record> archive; | |
| using _Checkpoint = typename decltype(archive)::value_type *; | |
| public: | |
| /** | |
| * @brief Opaque checkpoint type for partial rollback. | |
| * | |
| * Checkpoints represent a point in the history of assignments. You can | |
| * pass a checkpoint to reset() to roll back only changes made after that point. | |
| * A nullptr checkpoint represents the beginning (before any assignments). | |
| */ | |
| using Checkpoint = _Checkpoint; | |
| /** | |
| * @brief Default constructor. Creates an empty PhantomAssignments manager. | |
| */ | |
| PhantomAssignments() = default; | |
| /// @brief Copying is disabled (the manager holds references to external variables). | |
| PhantomAssignments(PhantomAssignments const&) = delete; | |
| /// @brief Copy assignment is disabled. | |
| PhantomAssignments& operator=(PhantomAssignments const&) = delete; | |
| /// @brief Move constructor. | |
| PhantomAssignments(PhantomAssignments&&) = default; | |
| /// @brief Move assignment. | |
| PhantomAssignments& operator=(PhantomAssignments&&) = default; | |
| /** | |
| * @brief Destructor. Behavior depends on AutoReset template parameter. | |
| * | |
| * - If AutoReset is true: Calls reset() to roll back all changes. | |
| * - If AutoReset is false: Calls commit() to finalize all changes. | |
| */ | |
| ~PhantomAssignments() { | |
| if constexpr (AutoReset_) { | |
| reset(); | |
| } else { | |
| commit(); | |
| } | |
| } | |
| /** | |
| * @brief Get a checkpoint representing the current state. | |
| * | |
| * @return A Checkpoint that can be passed to reset() later for partial rollback. | |
| * Returns nullptr if no assignments have been made. | |
| * | |
| * @note Thread-safe. | |
| * | |
| * @par Example | |
| * @code | |
| * pa.assign(x, 10); | |
| * auto ckpt = pa.current(); | |
| * pa.assign(y, 20); | |
| * pa.reset(ckpt); // Only 'y' is rolled back | |
| * @endcode | |
| */ | |
| Checkpoint current() const { | |
| std::lock_guard lock(mtx); | |
| return archive.empty() ? nullptr : &archive.front(); | |
| } | |
| /** | |
| * @brief Backup a variable's current value without modifying it. | |
| * | |
| * @tparam T Must be one of the managed types (Types_...). | |
| * @param lvalue Reference to the variable to back up. | |
| * @return Checkpoint for this backup operation. | |
| * | |
| * @note The variable's value is moved into the backup. After this call, | |
| * the original variable is in a moved-from state. You typically want | |
| * to use assign() instead unless you have special requirements. | |
| * @note Thread-safe. | |
| * | |
| * @par Example | |
| * @code | |
| * std::vector<int> vec = {1, 2, 3}; | |
| * pa.backup(vec); // vec is now in moved-from state | |
| * vec = {4, 5, 6}; // Manually assign new value | |
| * pa.reset(); // vec is restored to {1, 2, 3} | |
| * @endcode | |
| */ | |
| template <class T> | |
| std::enable_if_t<type_is_legal_v<T>, Checkpoint> | |
| backup(T& lvalue) { | |
| std::lock_guard lock(mtx); | |
| return &archive.emplace_front(std::make_pair( | |
| std::ref(lvalue), std::move(lvalue))); | |
| } | |
| /** | |
| * @brief Perform a phantom assignment with automatic backup. | |
| * | |
| * Backs up the current value of `lvalue`, then assigns `rvalue` to it. | |
| * The original value can be restored by calling reset(). | |
| * | |
| * @tparam T Must be one of the managed types (Types_...). | |
| * @param lvalue Reference to the variable to assign to. | |
| * @param rvalue The new value to assign (rvalue reference, will be moved). | |
| * @param precondition Optional condition that must be true; throws if false. | |
| * @return Checkpoint for this assignment operation. | |
| * | |
| * @throws precondition_failed if precondition is false. | |
| * @note Thread-safe. | |
| * | |
| * @par Example | |
| * @code | |
| * int x = 10; | |
| * pa.assign(x, 20); // x is now 20 | |
| * pa.reset(); // x is restored to 10 | |
| * @endcode | |
| * | |
| * @par Example with precondition | |
| * @code | |
| * // Only assign if condition is met; throws otherwise | |
| * pa.assign(x, 20, x > 0); | |
| * @endcode | |
| */ | |
| template <class T> | |
| std::enable_if_t<type_is_legal_v<T>, Checkpoint> | |
| assign(T& lvalue, T&& rvalue, bool precondition = true) { | |
| if (!precondition) | |
| throw precondition_failed(); | |
| std::lock_guard lock(mtx); | |
| Checkpoint&& ckpt = &archive.emplace_front(std::make_pair( | |
| std::ref(lvalue), std::move(lvalue))); | |
| lvalue = std::move(rvalue); | |
| return ckpt; | |
| } | |
| /** | |
| * @brief Perform a phantom assignment with automatic backup (const lvalue version). | |
| * | |
| * Same as assign(T&, T&&, bool) but accepts a const lvalue reference, | |
| * which will be copied into a temporary before being moved. | |
| * | |
| * @tparam T Must be one of the managed types (Types_...). | |
| * @param lvalue Reference to the variable to assign to. | |
| * @param rvalue The new value to assign (will be copied). | |
| * @param precondition Optional condition that must be true; throws if false. | |
| * @return Checkpoint for this assignment operation. | |
| * | |
| * @throws precondition_failed if precondition is false. | |
| * @note Thread-safe. | |
| */ | |
| template <class T> | |
| std::enable_if_t<type_is_legal_v<T>, Checkpoint> | |
| assign(T& lvalue, T const& rvalue, bool precondition = true) { | |
| return assign(lvalue, T(rvalue), precondition); | |
| } | |
| /** | |
| * @brief Register a callback hook to be called on reset or commit. | |
| * | |
| * Hooks allow you to execute custom cleanup code when the transaction | |
| * is reset (rolled back) or committed. | |
| * | |
| * @tparam OnReset If true, the hook runs on reset(); if false, runs on commit(). | |
| * @param destructor The callback function to execute. | |
| * | |
| * @note If destructor is empty (null), this call is a no-op. | |
| * @note Thread-safe. | |
| * | |
| * @par Example | |
| * @code | |
| * // Cleanup that should happen on rollback | |
| * pa.hook<true>([]{ | |
| * std::cout << "Transaction rolled back, cleaning up...\n"; | |
| * }); | |
| * | |
| * // Cleanup that should happen on commit (e.g., notify observers) | |
| * pa.hook<false>([]{ | |
| * std::cout << "Transaction committed!\n"; | |
| * }); | |
| * @endcode | |
| */ | |
| template <bool OnReset> | |
| void hook(Destructor const& destructor) { | |
| if (!destructor) | |
| return; | |
| std::lock_guard lock(mtx); | |
| archive.emplace_front(std::make_pair(destructor, OnReset)); | |
| } | |
| /** | |
| * @brief Roll back all assignments made after the given checkpoint. | |
| * | |
| * Restores all variables to their backed-up values, in reverse order | |
| * of assignment, stopping at (but not including) the given checkpoint. | |
| * Also executes any hooks registered with hook<true>() for the rolled-back portion. | |
| * | |
| * @param ckpt The checkpoint to roll back to. Pass nullptr to roll back everything. | |
| * | |
| * @note Thread-safe. | |
| * | |
| * @par Example | |
| * @code | |
| * int a = 1, b = 2; | |
| * pa.assign(a, 10); | |
| * auto ckpt = pa.current(); | |
| * pa.assign(b, 20); | |
| * | |
| * pa.reset(ckpt); // Only 'b' is rolled back to 2; 'a' remains 10 | |
| * @endcode | |
| */ | |
| void reset(Checkpoint const& ckpt) { | |
| std::lock_guard lock(mtx); | |
| while (!archive.empty() && &archive.front() != ckpt) { | |
| std::visit(overloaded { | |
| [](AssignRecord const& record) { | |
| std::visit([](auto const& assignment) { | |
| auto const& [lvalue, rvalue] = assignment; | |
| lvalue.get() = std::move(rvalue); | |
| }, record); | |
| }, | |
| [](HookRecord const& record) { | |
| if (record.second) { | |
| record.first(); | |
| } | |
| } | |
| }, archive.front()); | |
| archive.pop_front(); | |
| } | |
| } | |
| /** | |
| * @brief Roll back all assignments to their original values. | |
| * | |
| * Equivalent to `reset(nullptr)`. Restores all managed variables to their | |
| * values at the time of backup/assign, and executes all hooks registered | |
| * with hook<true>(). | |
| * | |
| * @note Thread-safe. | |
| * | |
| * @par Example | |
| * @code | |
| * int x = 10; | |
| * pa.assign(x, 20); | |
| * pa.assign(x, 30); | |
| * pa.reset(); // x is restored to 10 | |
| * @endcode | |
| */ | |
| void reset() { | |
| reset(nullptr); | |
| } | |
| /** | |
| * @brief Commit all changes, discarding backups. | |
| * | |
| * Finalizes all assignments by discarding their backups. The current values | |
| * of all variables become permanent (cannot be rolled back). Also executes | |
| * any hooks registered with hook<false>(). | |
| * | |
| * @note Thread-safe. | |
| * | |
| * @par Example | |
| * @code | |
| * int x = 10; | |
| * pa.assign(x, 20); | |
| * pa.commit(); // x is now permanently 20; cannot be rolled back | |
| * @endcode | |
| */ | |
| void commit() { | |
| std::lock_guard lock(mtx); | |
| while (!archive.empty()) { | |
| std::visit(overloaded { | |
| [](AssignRecord const&) {}, | |
| [](HookRecord const& record) { | |
| if (!record.second) { | |
| record.first(); | |
| } | |
| } | |
| }, archive.front()); | |
| archive.pop_front(); | |
| } | |
| } | |
| }; | |
| /** | |
| * @brief Specialization that flattens std::variant types into the type list. | |
| * | |
| * This allows you to pass a std::variant as a template argument, and its | |
| * contained types will be automatically extracted and added to the managed types. | |
| * | |
| * @par Example | |
| * @code | |
| * using NumericTypes = std::variant<int, float, double>; | |
| * PhantomAssignments<true, NumericTypes, std::string> pa; | |
| * // Equivalent to: PhantomAssignments<true, int, float, double, std::string> | |
| * | |
| * int i = 0; | |
| * float f = 0.0f; | |
| * std::string s = ""; | |
| * pa.assign(i, 42); // OK | |
| * pa.assign(f, 3.14f); // OK | |
| * pa.assign(s, "hello"); // OK | |
| * @endcode | |
| */ | |
| template <bool AutoReset_, class... Types_, class... ExtraTypes_> | |
| struct PhantomAssignments<AutoReset_, std::variant<Types_...>, ExtraTypes_...> : | |
| PhantomAssignments<AutoReset_, ExtraTypes_..., Types_...> {}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment