Skip to content

Instantly share code, notes, and snippets.

@XDflight
Created March 5, 2026 13:32
Show Gist options
  • Select an option

  • Save XDflight/f59520a0881d54741ae644b4a6b2225a to your computer and use it in GitHub Desktop.

Select an option

Save XDflight/f59520a0881d54741ae644b4a6b2225a to your computer and use it in GitHub Desktop.
A transactional C++ variable assignment manager with rollback/commit semantics.
/**
* @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