Documentation | Examples | English | Русский | Changelog
A small header-only C++20 library for tracking selected object fields. Compare snapshots, receive deferred change callbacks, inspect container elements, and restore earlier states. No external runtime dependencies.
Requires a C++20 compiler. CMake builds require CMake 3.20 or newer.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel 2
ctest --test-dir build --output-on-failure
./build/vardiff_playerOn multi-configuration generators such as Visual Studio, add --config Release
to the build and -C Release to CTest; examples are under build/Release/.
The example prints hp: 100 -> 75.
#include <vardiff/vardiff.hpp>
#include <iostream>
struct Player {
int hp = 100;
int coins = 0;
};
int main() {
Player player;
auto watcher = vardiff::track(
player,
vardiff::field("hp", &Player::hp),
vardiff::field("coins", &Player::coins)
);
player.hp = 75;
if (watcher.changed()) {
for (const auto& change : watcher.diff()) {
std::cout << change.name << ": "
<< change.before << " -> "
<< change.after << '\n';
}
}
watcher.snapshot();
}Full source: examples/player.cpp.
For a direct compiler invocation:
mkdir -p build-direct
c++ -std=c++20 -Iinclude examples/player.cpp -o build-direct/player
./build-direct/player- Explicit field selection with
field()andtrack(). - Non-mutating
changed()anddiff(); owned copies of snapshot and diff values. - Deferred callbacks through
on_change()andpoll(). - Path exclusions, custom equality predicates, and configurable automatic snapshots.
- Bounded history with
rollback(),undo(), andredo(). - Copyable custom types and recursive comparison of explicitly registered fields.
- Element-level comparison of
std::vector,std::optional, andstd::array. - Comparison of two independent objects.
Using a fresh Player player; and the Player type above:
auto watcher = vardiff::track(
player, vardiff::watch_options{.auto_snapshot = true, .history_limit = 32},
vardiff::field("hp", &Player::hp),
vardiff::field("coins", &Player::coins));
watcher.ignore("coins");
watcher.on_change([](const auto& changes) {
for (const auto& change : changes) {
std::cout << change.name << ": " << change.before
<< " -> " << change.after << '\n';
}
});
watcher.on_change("hp", [](const vardiff::change& change) {
std::cout << "new hp: " << change.after.get<int>() << '\n';
});
player.hp = 75;
watcher.poll();Callbacks run only during poll(). Assignment, changed(), diff(),
snapshot(), and history restoration do not dispatch callbacks.
By default, a successful poll() with changes saves the state observed before
callbacks. Callback edits remain pending for the next poll. Disable automatic
updates with watcher.set_auto_snapshot(false); then differences are reported
again until snapshot() is called.
See examples/events.cpp for a complete example, including a custom equality predicate.
Register fields before first use; registered types do not need operator==:
struct Item {
std::string name;
double price;
};
template <>
struct vardiff::schema<Item> {
static auto fields() {
return std::make_tuple(
vardiff::field("name", &Item::name),
vardiff::field("price", &Item::price));
}
};Fields containing Item, std::vector<Item>, std::optional<Item>, or
std::array<Item, N> are compared recursively. Value trees with
std::vector<Node> children are supported. A registered root object can be
tracked with vardiff::track(object).
Change names are paths such as items[2].price. Containers are compared by
index. Added or removed elements use change_kind::added / removed;
the absent side has value.has_value() == false and prints <missing>.
Use watcher.ignore("items[1].price") to exclude one leaf, or
watcher.ignore("items") to exclude a whole branch.
Complete example: examples/nested.cpp.
watcher.undo(); // Previous saved state; false at the boundary.
watcher.redo(); // Next saved state.
watcher.rollback(); // Same as undo().
watcher.rollback(0); // A chosen retained snapshot.
watcher.set_history_limit(16); // Includes the current snapshot.
const auto states = watcher.history(); // Independent copies of value tuples.
const auto count = watcher.history_size();
const auto position = watcher.history_position();The initial state is retained. The default limit is 64 snapshots. A new snapshot after undo discards the redo branch. Restoration discards unsaved edits and requires assignable fields.
before and after preserve their types: use change.before.get<int>(),
change.after.get<std::string>(), or get<MyType>(). Wrong-type access throws
std::bad_variant_access. Stream output uses the stored type's operator<<
when available.
Custom comparison is the third argument of field(): its predicate returns
true for equal values. It overrides the entire field's comparison, producing
one atomic change. Comparators must be pure and callable as const.
auto changes = vardiff::diff(before, after,
vardiff::field("hp", &Player::hp),
vardiff::field("coins", &Player::coins));
bool different = vardiff::changed(before, after, vardiff::field("hp", &Player::hp));For registered types, use vardiff::diff(before, after) and
vardiff::changed(before, after).
deep_diff() adds sequence moves and identity matching to the shared recursive
engine. It supports registered structures, std::vector, std::array,
std::map, std::unordered_map, std::optional, and nested combinations.
// Item and its schema are defined in examples/deep_diff.cpp.
const std::vector<Item> before{{1, 10}, {2, 20}, {3, 30}};
const std::vector<Item> after{{2, 20}, {3, 30}, {1, 12}};
auto changes = vardiff::deep_diff(before, after, vardiff::key(&Item::id));
// Move [0] -> [2], then [2].count: 10 -> 12.deep_options controls moves, duplicate-key handling, ignored paths and maximum
depth. Duplicate keys match in occurrence order by default, or can be rejected.
Map paths include keys (equipment["hand"].count); nested sequence paths include
destination indices (inventory[2].count). Moves carry source/destination paths
and indices without copying the moved object. Existing diff() and watcher
sequence behavior stays index-based; they also gain recursive map support.
See Deep Diff contracts and complexity, the complete example and benchmarks.
Vendor the source and use:
add_subdirectory(path/to/vardiff)
target_link_libraries(my_app PRIVATE vardiff::vardiff)Or install a package:
cmake --install build --prefix ./installThen consume it with:
find_package(vardiff 0.2 CONFIG REQUIRED)
target_link_libraries(my_app PRIVATE vardiff::vardiff)Pass the install prefix to the consumer's CMake configure command using
-DCMAKE_PREFIX_PATH=/path/to/install. The installed package is relocatable.
| Option | Standalone default | As a subdirectory |
|---|---|---|
VARDIFF_BUILD_TESTS |
ON | OFF |
VARDIFF_BUILD_EXAMPLES |
ON | OFF |
VARDIFF_BUILD_BENCHMARKS |
OFF | OFF |
VARDIFF_WARNINGS_AS_ERRORS |
OFF | OFF |
Warning options apply only to vardiff's own executables, not to consumers.
The watched object is borrowed and must remain alive at the same address.
Snapshots use the normal copy semantics of each field's type.
Only explicitly registered fields are traversed. The default floating-point
comparison is exact operator==, so NaN remains different even after a snapshot.
There is no background observation, automatic reflection, pointer-graph traversal, JSON, or thread synchronization.
The CI workflow is configured for GCC, Clang, Apple Clang, MSVC, sanitizers, and an installed-package consumer.
AI-assisted tools were used during the development of VarDiff for code generation, refactoring, and documentation.
All contributions are reviewed and tested before being merged.
Copyright 2026 Dmitry. Licensed under Apache-2.0. Attribution notices are in NOTICE. Include both files when redistributing the library.