- Overview
- Core Concepts
- State Tree Structure
- Basic Operations
- Advanced Features
- Complete API Reference
- Usage Examples
- Exception Handling
- Performance Considerations
- Testing
The cvc::state class provides a thread-safe, hierarchical key-value store with signal/slot notifications and futures-based async operations. It serves as the backbone for application state management in libcvc, supporting:
- Hierarchical organization: Dot-notation paths create tree structures
- Type-safe storage: Template methods with automatic conversion
- Async notifications: Signal/slot mechanism for value changes
- Futures API: Producer-consumer patterns and blocking waits
- Property system: Metadata and attributes per state node
- Thread safety: All operations protected by mutex locks
- Serialization: JSON import/export for persistence
Each cvc::app owns its own state tree. Get the root with the
app::root() shorthand:
cvc::app app;
cvc::state& root = app.root();
root("app.window.width").value(1920);app.root() is exactly equivalent to cvc::state::instance(app) —
the second spelling is also fine if it reads better in your
context, e.g. inside templates that already accept an app&.
The examples in this document assume cvc::app app; is in scope
and use either app.root() or cvc::state::instance(app) as the
root accessor. Earlier versions of libcvc exposed the root through
a global cvcstate macro / state::instance() zero-arg singleton;
those have been removed — every caller must now reach the root
through its owning app.
State nodes are organized in a hierarchical tree structure using dot-notation paths:
root
├── app
│ ├── window
│ │ ├── width (value: 1920)
│ │ └── height (value: 1080)
│ └── theme (value: "dark")
└── data
├── volume.filename (value: "scan.rawiv")
└── geometry.mesh (data: geometry object)
- Values: Convertible types (int, double, string) - stored internally as strings
- Data: Arbitrary C++ objects stored as
boost::any- no automatic conversion
State nodes emit signals when values/data change, enabling reactive programming:
state::instance(app)("sensor.temperature").valueChanged.connect([](std::string val) {
std::cout << "Temperature: " << val << std::endl;
});// Dot notation creates parent nodes automatically
state::instance(app)("level1.level2.level3.key");
// Equivalent to:
// state::instance(app)("level1")
// .child("level2")
// .child("level3")
// .child("key")// Setting a deep path creates all intermediate nodes
state::instance(app)("new.path.to.value").value(42);
// Creates hierarchy:
// root -> new -> path -> to -> value#include <cvc/state.h>
// Global convenience function creates/accesses nodes
state s1 = state::instance(app)("app.window.width");
// Direct construction
state s2("app.window.height");
// Access via parent
state parent = state::instance(app)("app.window");
state child = parent.child("title");// Set value (converted to string internally)
state::instance(app)("count").value(42);
state::instance(app)("ratio").value(3.14159);
state::instance(app)("name").value(std::string("libcvc"));
// Get value with type conversion
int count = state::instance(app)("count").value<int>();
double ratio = state::instance(app)("ratio").value<double>();
std::string name = state::instance(app)("name").value<std::string>();
// Check if value exists
if (state::instance(app)("optional.key").initialized()) {
int val = state::instance(app)("optional.key").value<int>();
}
// Get raw string value
std::string raw = state::instance(app)("count").value(); // "42"// Store arbitrary C++ objects
struct Config {
int width, height;
std::string theme;
};
Config cfg{1920, 1080, "dark"};
state::instance(app)("app.config").data(cfg);
// Retrieve with exact type match
try {
Config loaded = state::instance(app)("app.config").data<Config>();
} catch (const cvc::type_conversion_error& e) {
std::cerr << "Type mismatch: " << e.what() << std::endl;
}
// Check if data exists
if (state::instance(app)("app.config").has_data()) {
Config cfg = state::instance(app)("app.config").data<Config>();
}// Set property (metadata for state node)
state::instance(app)("value").property("units", "meters");
state::instance(app)("value").property("min", "0.0");
state::instance(app)("value").property("max", "100.0");
// Get property
std::string units = state::instance(app)("value").property("units");
// Check if property exists
if (state::instance(app)("value").has_property("units")) {
// Property exists
}
// Get all property keys
std::vector<std::string> keys = state::instance(app)("value").property_keys();The futures API enables async/await-style programming with state values, providing blocking waits, timeout support, callbacks, and future objects for advanced control. This enables producer-consumer patterns and async message passing through the state tree.
Key Features:
- Blocking waits: Wait indefinitely for a value to be set
- Timeout support: Wait with configurable timeout
- Callbacks: Register callbacks that fire when values change
- Type-safe: Fully templated with type conversion
- Thread-safe: All operations use proper synchronization
// Wait indefinitely for a value to be set
int result = state::instance(app)("computation.result").wait_for_value<int>();
// Wait with timeout (throws timeout_error on timeout)
double value = state::instance(app)("sensor.reading")
.wait_for_value<double>(boost::chrono::seconds(5));
// Wait for data object
MyStruct data = state::instance(app)("queue.item").wait_for_data<MyStruct>();// Register callback that fires when value changes
int current = state::instance(app)("counter").value<int>([](int newValue) {
std::cout << "Counter changed to: " << newValue << std::endl;
});
// Callback fires asynchronously whenever counter is updated
state::instance(app)("counter").value(current + 1); // Triggers callbackThe value_future<T>() method returns a state_future<T> object for advanced control:
// Get a future object for advanced control
auto future = state::instance(app)("async.result").value_future<std::string>();
// Non-blocking check
if (future.is_ready()) {
std::string val = future.get();
}
// Wait with timeout
if (future.wait_for(boost::chrono::milliseconds(500))) {
std::string val = future.get();
} else {
std::cout << "Still waiting..." << std::endl;
}
// Get with timeout (throws on timeout)
std::string val = future.get_for(boost::chrono::seconds(10));
// Block indefinitely
std::string val = future.get();// Consumer thread waits for producer
boost::thread consumer([]() {
// Blocks until producer sets the value
int result = state::instance(app)("work.result").wait_for_value<int>();
std::cout << "Got result: " << result << std::endl;
});
// Producer thread does work and sets result
boost::thread producer([]() {
int result = do_expensive_computation();
state::instance(app)("work.result").value(result);
});
consumer.join();
producer.join();// Multiple threads can wait on the same state value
std::vector<boost::thread> consumers;
for (int i = 0; i < 5; ++i) {
consumers.emplace_back([i]() {
// All threads will be notified when value is set
int value = state::instance(app)("broadcast.value").wait_for_value<int>();
std::cout << "Consumer " << i << " got: " << value << std::endl;
});
}
// Producer sets value once - all consumers wake up
boost::this_thread::sleep_for(boost::chrono::milliseconds(100));
state::instance(app)("broadcast.value").value(42);
for (auto& t : consumers) {
t.join();
}Before (inefficient polling):
// Inefficient busy-wait polling
while (!state::instance(app)("key").initialized()) {
boost::this_thread::sleep_for(boost::chrono::milliseconds(10));
}
int val = state::instance(app)("key").value<int>();After (efficient futures):
// Efficient blocking wait with condition variables
int val = state::instance(app)("key").wait_for_value<int>();Synchronization:
- Uses
boost::condition_variablefor blocking waits - Uses
boost::signals2for callbacks - All operations are protected by mutex locks
- Supports multiple waiters on the same state node
Exception Safety:
- Timeout operations throw
cvc::timeout_errorwith descriptive messages timeout_errorinherits fromcvc::exception(which inherits fromboost::exception)- Type conversion errors in callbacks are silently caught
- Move semantics for
state_futureprevent accidental copies
Performance:
- Callbacks execute in the thread that modifies the state
- Futures wake all waiting threads when value changes
- Minimal overhead when features are not used
- Condition variables efficiently wait without busy-spinning
State nodes emit signals when values/data/properties change:
// Connect to value change signal
state::instance(app)("config.theme").valueChanged.connect([](std::string newTheme) {
applyTheme(newTheme);
});
// Connect to data change signal
state::instance(app)("mesh.data").dataChanged.connect([]() {
// Data changed - reload mesh
cvc::geometry mesh = state::instance(app)("mesh.data").data<cvc::geometry>();
renderMesh(mesh);
});
// Connect to property change signal
state::instance(app)("slider").propertyChanged.connect([](std::string key, std::string value) {
std::cout << "Property '" << key << "' = " << value << std::endl;
});
// Signals use boost::signals2 - thread-safe and multi-subscriberCallbacks that modify state can create cascading chains or infinite loops if not carefully designed. Here are common pitfalls and safe patterns to avoid them.
1. Infinite Callback Loops
Two states that listen to each other create an infinite loop:
// ❌ DANGEROUS: Infinite loop
auto connA = state::instance(app)("nodeA").valueChanged.connect([]() {
state::instance(app)("nodeB").value("trigger"); // Triggers nodeB's callback
});
auto connB = state::instance(app)("nodeB").valueChanged.connect([]() {
state::instance(app)("nodeA").value("trigger"); // Triggers nodeA's callback → LOOP!
});
state::instance(app)("nodeA").value("start"); // Stack overflow or hang!2. Deep Callback Chains
Long chains can exhaust stack space (typically >100-1000 levels):
// ⚠️ RISKY: Deep chain without limits
for (int i = 0; i < 1000; ++i) {
std::string current = "node" + std::to_string(i);
std::string next = "node" + std::to_string(i + 1);
state::instance(app)(current).valueChanged.connect([next, i]() {
state::instance(app)(next).value(i); // Each callback triggers the next
});
}3. Uncontrolled Cascade Effects
Exponential fan-out can cause performance issues:
// ⚠️ RISKY: Exponential callback explosion
state::instance(app)("parent").valueChanged.connect([]() {
for (int i = 0; i < 100; ++i) {
state::instance(app)("child" + std::to_string(i)).value(i);
// If each child also triggers more callbacks...
}
});Pattern 1: Iteration Counter Guard
Limit recursive calls with counters:
// ✅ SAFE: Counter prevents infinite loops
std::atomic<int> nodeA_count(0);
std::atomic<int> nodeB_count(0);
const int MAX_ITERATIONS = 10;
auto connA = state::instance(app)("nodeA").valueChanged.connect([&nodeA_count]() {
int count = nodeA_count.fetch_add(1);
if (count < MAX_ITERATIONS) {
state::instance(app)("nodeB").value(count);
}
});
auto connB = state::instance(app)("nodeB").valueChanged.connect([&nodeB_count]() {
int count = nodeB_count.fetch_add(1);
if (count < MAX_ITERATIONS) {
state::instance(app)("nodeA").value(count);
}
});Pattern 2: Processing Flag (Re-entry Prevention)
Use atomic flags to prevent re-entry:
// ✅ SAFE: Processing flag prevents re-entry
struct GuardedCallback {
std::atomic<bool> processing{false};
void operator()(const std::string& trigger_key) {
// Try to acquire processing flag
bool expected = false;
if (!processing.compare_exchange_strong(expected, true)) {
return; // Already processing, skip to prevent loop
}
state::instance(app)(trigger_key).value("update");
processing.store(false);
}
};
GuardedCallback guardA, guardB;
state::instance(app)("nodeA").valueChanged.connect([&guardA]() { guardA("nodeB"); });
state::instance(app)("nodeB").valueChanged.connect([&guardB]() { guardB("nodeA"); });Pattern 3: Change Detection Guard
Only trigger when values actually change:
// ✅ SAFE: Only trigger on actual changes
struct ChangeDetector {
std::string last_value;
boost::mutex mutex;
bool hasChanged(const std::string& new_value) {
boost::lock_guard<boost::mutex> lock(mutex);
if (last_value == new_value) return false;
last_value = new_value;
return true;
}
};
ChangeDetector detector;
state::instance(app)("watched").valueChanged.connect([&detector]() {
std::string value = state::instance(app)("watched").value();
if (detector.hasChanged(value)) {
state::instance(app)("dependent").value(value);
}
});Pattern 4: Depth Tracking
Monitor and limit call stack depth:
// ✅ SAFE: Depth tracking prevents stack overflow
thread_local int callback_depth = 0;
const int MAX_CALLBACK_DEPTH = 50;
struct DepthGuard {
bool exceeded = false;
DepthGuard() {
if (++callback_depth > MAX_CALLBACK_DEPTH) {
exceeded = true;
--callback_depth;
}
}
~DepthGuard() {
if (!exceeded) --callback_depth;
}
};
state::instance(app)("node").valueChanged.connect([]() {
DepthGuard guard;
if (guard.exceeded) {
std::cerr << "Warning: Max callback depth exceeded!\n";
return;
}
processCallback();
});Pattern 5: Async Operations
Break recursion using thread pools:
// ✅ SAFE: Async breaks the call stack
state::instance(app)("nodeA").valueChanged.connect([]() {
app.thread("worker", []() {
state::instance(app)("nodeB").value("update");
});
});- Avoid Circular Dependencies: Design state relationships as directed acyclic graphs (DAGs)
- Use Convergent Callbacks: Design callbacks that converge to stable states
- Make Callbacks Idempotent: Same input should produce same result when called multiple times
- Document Callback Chains: Comment expected sequences and max depth
- Monitor Performance: Track callback execution in production code
Based on testing with PerformanceCallbackChains:
- Linear chains: Safe up to ~100 levels
- Fan-out factor: Keep below 50 children per callback when those children have callbacks
- Iteration limits: Set to 10-20 for mutually triggering callbacks
- Concurrent callbacks: Tested safe with 1000+ simultaneous callbacks
class ValidationPipeline {
private:
std::atomic<bool> validating{false};
std::vector<boost::signals2::connection> connections;
public:
void setup() {
// Step 1: Input → Validation (with re-entry guard)
connections.push_back(
state::instance(app)("pipeline.input").valueChanged.connect([this]() {
bool expected = false;
if (!validating.compare_exchange_strong(expected, true)) {
return; // Already validating
}
std::string input = state::instance(app)("pipeline.input").value();
bool valid = validateInput(input);
state::instance(app)("pipeline.valid").value(valid);
validating.store(false);
})
);
// Step 2: Validation → Processing (conditional, no loop back)
connections.push_back(
state::instance(app)("pipeline.valid").valueChanged.connect([]() {
bool valid = state::instance(app)("pipeline.valid").value<bool>();
if (valid) {
std::string input = state::instance(app)("pipeline.input").value();
std::string processed = processInput(input);
state::instance(app)("pipeline.output").value(processed);
}
})
);
// Step 3: Output → UI Update (terminal, no further callbacks)
connections.push_back(
state::instance(app)("pipeline.output").valueChanged.connect([]() {
std::string output = state::instance(app)("pipeline.output").value();
updateUI(output); // Terminal: no further state changes
})
);
}
~ValidationPipeline() {
for (auto& conn : connections) conn.disconnect();
}
};This pipeline is safe because:
- ✅ Re-entry guard prevents loops
- ✅ DAG structure (input → valid → output)
- ✅ Terminal callbacks don't trigger further changes
- ✅ Conditional execution prevents unnecessary chains
All cvc::state operations are thread-safe via mutex protection:
// Multiple threads can safely access the same state
boost::thread t1([](){ state::instance(app)("shared").value(1); });
boost::thread t2([](){ state::instance(app)("shared").value(2); });
boost::thread t3([](){ int v = state::instance(app)("shared").value<int>(); });
t1.join(); t2.join(); t3.join();
// Futures enable producer-consumer patterns
boost::thread producer([](){
state::instance(app)("result").value(compute());
});
boost::thread consumer([](){
int result = state::instance(app)("result").wait_for_value<int>();
});Important: When using state_object<T> or connecting to signals, callbacks execute in separate threads spawned asynchronously. This design has an important implication for reading state values within callbacks.
The template method value<T>(const T& v) updates state in two steps:
- Convert value to string:
std::string str_value = boost::lexical_cast<std::string>(v) - Acquire lock and atomically update
_valueTypeNameand_value - Release lock and fire signals
However, before December 2025, there was a race condition where _valueTypeName and _value were updated in separate lock scopes:
// OLD CODE (had race condition):
template <class T> state& value(const T& v) {
{
boost::mutex::scoped_lock lock(_mutex);
_valueTypeName = app.dataTypeName<T>();
} // Lock released here - RACE WINDOW!
return value(boost::lexical_cast<std::string>(v), false);
}This created a window where async callbacks could read:
_valueTypeName= "int" (new type)_value= "false" (old string value from previous bool)
When the callback tried value<int>(), which does boost::lexical_cast<int>("false"), it would throw boost::bad_lexical_cast.
The template method now updates both fields atomically under a single lock:
// CURRENT CODE (atomic update):
template <class T> state& value(const T& v) {
std::string str_value = boost::lexical_cast<std::string>(v);
{
boost::mutex::scoped_lock lock(_mutex);
if(_value == str_value) return *this; // Early return if unchanged
_valueTypeName = app.dataTypeName<T>();
_value = str_value;
_lastMod = boost::posix_time::microsec_clock::universal_time();
_initialized = true;
_valueCondition.notify_all();
}
valueChanged();
if(parent()) parent()->childChanged(fullName());
return *this;
}Key improvements:
- ✅
_valueTypeNameand_valueupdated atomically in single lock scope - ✅ Early return for unchanged values (preserves
_lastModsemantics) - ✅ Lexical cast performed before taking lock (avoid holding lock during conversion)
- ✅ Condition variable notification for futures API
Even with the atomic fix, defensive coding in callbacks is recommended:
class MyObject : public state_object<MyObject> {
protected:
virtual void handleStateChanged(const std::string& childState) override {
if (childState.find("counter") != std::string::npos) {
try {
// Defensive: catch any unexpected conversion errors
int value = getState("counter").value<int>();
processValue(value);
} catch (const boost::bad_lexical_cast& e) {
// Log and handle gracefully
app.log(1, "Warning: Failed to convert counter value");
}
}
}
};Why defensive coding is still valuable:
- Handles edge cases during rapid state changes
- Graceful degradation if future refactoring introduces issues
- Better error messages for debugging
- Prevents crashes in production code
The test suite includes specific tests for this scenario:
TEST(StateTest, StateObjectAsyncHandlers) {
ConfigurationObject config;
int initialCount = config.resizeCount.load();
// Rapid changes that previously triggered race condition
for (int i = 0; i < 10; ++i) {
config.getState("width").value(1920 + i * 100);
}
// Wait for async handlers to complete
boost::this_thread::sleep_for(boost::chrono::milliseconds(1000));
// Verify handlers executed without crashes
int finalCount = config.resizeCount.load();
EXPECT_GT(finalCount, initialCount);
}This test rapidly changes integer values, which previously would cause bad_lexical_cast exceptions in the async handlers. The fix ensures all handlers can safely read the updated values.
// Export state tree to JSON string
std::string json = state::instance(app).dump();
// Import from JSON (merges with existing state)
state::instance(app).read(json_string);
// Serialization preserves:
// - Value data (as strings)
// - Properties (key-value pairs)
// - Tree hierarchy
// Note: Data objects (boost::any) are NOT serializedThe cvc::state_object<T> template class provides a convenient base class for objects that want to store their member data in the state tree. This enables automatic state monitoring, easy persistence, and external control of object state.
Key Features:
- Automatic state registration: Each instance gets a unique state path based on class name and memory address
- Change notifications:
handleStateChanged()callback fires when any child state changes - Thread-safe updates: State changes trigger async handlers in separate threads
- Easy access: Convenient
getState()andstateName()helper methods - Type registration: Automatically registers the class type with the bound
cvc::app
#include <cvc/state_object.h>
// Inherit from state_object<YourClass>
class Configuration : public state_object<Configuration> {
public:
Configuration() {
// Initialize default values
getState("width").value(1920);
getState("height").value(1080);
getState("fullscreen").value(false);
}
protected:
// Override to handle state changes
virtual void handleStateChanged(const std::string& childState) override {
if (childState == "width" || childState == "height") {
resizeWindow(
getState("width").value<int>(),
getState("height").value<int>()
);
} else if (childState == "fullscreen") {
setFullscreen(getState("fullscreen").value<bool>());
}
}
private:
void resizeWindow(int w, int h) { /* ... */ }
void setFullscreen(bool fs) { /* ... */ }
};Other parts of your application can access and modify object state using the generated state path:
Configuration* config = new Configuration();
// State path format: <TypeName>/<InstanceAddress>/<ChildState>
// Example: "Configuration/0x7f8a1c000a10/width"
// Access via the object
config->getState("width").value(2560); // Triggers handleStateChanged()
// Or access globally if you know the path
std::string path = config->stateName("width");
state::instance(app)(path).value(2560); // Same effectThe state tree integration makes it easy to monitor and log state changes:
class DataProcessor : public state_object<DataProcessor> {
public:
DataProcessor() {
// Set up properties
getState("status").value("idle");
getState("progress").value(0.0);
getState("error_count").value(0);
}
void processData(const std::vector<double>& data) {
getState("status").value("processing");
for (size_t i = 0; i < data.size(); ++i) {
try {
processItem(data[i]);
getState("progress").value(
static_cast<double>(i) / data.size()
);
} catch (const std::exception& e) {
int count = getState("error_count").value<int>();
getState("error_count").value(count + 1);
}
}
getState("status").value("complete");
getState("progress").value(1.0);
}
protected:
virtual void handleStateChanged(const std::string& childState) override {
// Log all state changes
app.log(2, str(boost::format("DataProcessor: %s = %s")
% childState
% getState(childState).value()));
// React to specific changes
if (childState == "error_count") {
int errors = getState("error_count").value<int>();
if (errors > 10) {
app.log(0, "Too many errors, halting processing");
// Take corrective action
}
}
}
private:
void processItem(double value) { /* ... */ }
};State changes automatically spawn threads for handleStateChanged(), making the pattern safe for concurrent updates:
class Renderer : public state_object<Renderer> {
public:
Renderer() {
getState("camera.position").value("0,0,10");
getState("camera.target").value("0,0,0");
getState("render_mode").value("solid");
}
protected:
virtual void handleStateChanged(const std::string& childState) override {
// This runs in a separate thread for each state change
// Safe to perform expensive operations here
if (childState.find("camera.") == 0) {
updateCameraFromState();
requestRedraw();
} else if (childState == "render_mode") {
std::string mode = getState("render_mode").value();
setRenderMode(mode);
requestRedraw();
}
}
private:
void updateCameraFromState() {
// Parse and update camera
std::string pos = getState("camera.position").value();
std::string target = getState("camera.target").value();
// ... apply to camera
}
void setRenderMode(const std::string& mode) { /* ... */ }
void requestRedraw() { /* ... */ }
};
// Multiple threads can safely update the renderer's state
boost::thread t1([&renderer]() {
renderer.getState("camera.position").value("5,5,5");
});
boost::thread t2([&renderer]() {
renderer.getState("render_mode").value("wireframe");
});
t1.join();
t2.join();
// Both state changes trigger handleStateChanged() in separate threadsSince state_object integrates with the state tree, persistence is automatic:
class AppSettings : public state_object<AppSettings> {
public:
void loadDefaults() {
getState("window.width").value(1920);
getState("window.height").value(1080);
getState("theme").value("dark");
getState("language").value("en");
}
void save(const std::string& filename) {
// Export this object's state subtree to JSON
std::string json = getState().dump();
std::ofstream file(filename);
file << json;
}
void load(const std::string& filename) {
std::ifstream file(filename);
std::string json((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
getState().read(json);
// handleStateChanged() will be called for each restored value
}
protected:
virtual void handleStateChanged(const std::string& childState) override {
// React to loaded settings
if (childState == "theme") {
applyTheme(getState("theme").value());
}
}
};The state_object<T> template provides these methods:
Constructor:
- Registers the type with the bound
cvc::app - Sets up automatic state change monitoring
- Each instance gets a unique state path:
<TypeName>/<InstanceAddress>
Protected Methods:
// Override to handle state changes
virtual void handleStateChanged(const std::string& childState);Public Methods:
// Get state path for this instance (with optional child)
std::string stateName(const std::string& childState = std::string()) const;
// Access state tree for this instance
state& getState(const std::string& s = std::string()) const;Destructor:
- Disconnects state change monitoring
- State tree data persists until explicitly cleared
Thread Safety:
- Each
handleStateChanged()call runs in its own thread - Thread name format:
<statePath>_stateChanged - Non-blocking: state modifications return immediately
- Multiple simultaneous state changes spawn multiple handler threads
State Path Format:
<TypeName>/<InstanceAddress>/<ChildState>
Example: Configuration/0x7f8a1c000a10/window.width
Performance Considerations:
- Keep
handleStateChanged()implementations lightweight - Heavy operations are fine (they run in separate threads)
- Avoid modifying the same state from within
handleStateChanged()(potential recursion)
Best Practices:
- Initialize default state values in constructor
- Use
handleStateChanged()to react to external modifications - Use child state paths for logical grouping (e.g., "window.width", "window.height")
- Add logging in
handleStateChanged()for debugging - Use state properties for metadata (units, ranges, descriptions)
- Use batching for multiple related state changes (see below)
- Use
waitForHandlers()instead of arbitrary sleeps in tests
The state_object template supports batching of state changes to prevent thread floods when multiple values are modified rapidly. Without batching, each state change spawns a new handler thread immediately.
ConfigurationObject config;
// Each change spawns a separate handler thread immediately
config.getState("width").value(1024); // Thread 1 spawns
config.getState("width").value(1280); // Thread 2 spawns
config.getState("width").value(1920); // Thread 3 spawns
config.getState("height").value(768); // Thread 4 spawns
config.getState("height").value(1080); // Thread 5 spawns
// Result: 5 handler threads, even though we only care about final values!Issues:
- Thread flood: Hundreds of threads for rapid UI updates
- Race conditions: Handlers may execute out of order
- Resource waste: Processing intermediate values we don't need
- Unpredictable timing: Hard to know when handlers complete
ConfigurationObject config;
{
state_change_batch_scope<ConfigurationObject> batch(config);
config.getState("width").value(1024); // Queued
config.getState("width").value(1280); // Replaces previous
config.getState("width").value(1920); // Replaces previous
config.getState("height").value(768); // Queued
config.getState("height").value(1080); // Replaces previous
} // Scope ends: Only 2 threads spawn (one for width, one for height)
config.waitForHandlers(); // Wait for completionBenefits:
- ✅ Only 2 threads instead of 5
- ✅ Handlers see final values, not intermediate ones
- ✅ Deterministic ordering with explicit flush points
- ✅
waitForHandlers()provides clean synchronization
RAII Scope (Recommended):
template <class This>
class state_change_batch_scope
{
public:
explicit state_change_batch_scope(state_object<This>& obj);
~state_change_batch_scope(); // Auto-flushes
void flush(); // Manual flush before scope ends
};Manual Batching:
// state_object<T> methods
void beginBatch(); // Start batching
void endBatch(); // Flush pending changes
void waitForHandlers(); // Wait for all handlers to complete1. Basic RAII Pattern (Recommended):
void updateWindowSize(ConfigurationObject& config, int w, int h) {
state_change_batch_scope<ConfigurationObject> batch(config);
config.getState("width").value(w);
config.getState("height").value(h);
} // Handlers spawn here
config.waitForHandlers();2. Manual Flush:
{
state_change_batch_scope<ConfigurationObject> batch(config);
config.getState("width").value(1920);
config.getState("height").value(1080);
batch.flush(); // Spawn handlers NOW
config.waitForHandlers(); // Wait for completion
// More changes won't spawn (already flushed)
config.getState("width").value(2560);
}3. Nested Batching:
{
state_change_batch_scope<ConfigurationObject> outer(config);
config.getState("width").value(800);
{
state_change_batch_scope<ConfigurationObject> inner(config);
config.getState("width").value(1024);
config.getState("height").value(768);
} // Inner ends, outer still active - no handlers yet
config.getState("width").value(1920); // Final value
} // Outer ends - NOW handlers spawn4. Rapid Updates (UI/Animation):
void animateResize(ConfigurationObject& config) {
state_change_batch_scope<ConfigurationObject> batch(config);
for (int i = 0; i < 100; ++i) {
config.getState("width").value(1920 + i);
config.getState("height").value(1080 + i);
}
// Without batching: 200 threads!
// With batching: 2 threads (final width and height)
}✅ Use batching when:
- Multiple related changes happen at once (window resize, config loading)
- Rapid updates from user input or animation
- Loading/restoring state from files
- Testing - eliminates arbitrary sleeps, provides deterministic timing
❌ Don't use batching when:
- Single isolated change - no benefit
- Changes need immediate processing
Without Batching (100 width updates):
- Threads spawned: 100
- Time: ~500-1000ms
- Memory: 100 thread stacks
With Batching (100 width updates):
- Threads spawned: 1
- Time: ~5-10ms
- Memory: 1 thread stack
Result: ~100x fewer threads, ~100x faster
- Each
state_objecthas its own independent batch queue - Multiple threads can batch changes to different objects simultaneously
- Nested batching is thread-safe within a single object
waitForHandlers()is thread-safe from any thread
Before:
TEST(MyTest, ConfigChanges) {
ConfigurationObject config;
config.getState("width").value(1920);
// Arbitrary sleep - fragile on slow systems
boost::this_thread::sleep_for(boost::chrono::milliseconds(500));
EXPECT_EQ(config.lastWidth, 1920);
}After:
TEST(MyTest, ConfigChanges) {
ConfigurationObject config;
{
state_change_batch_scope<ConfigurationObject> batch(config);
config.getState("width").value(1920);
}
config.waitForHandlers(); // Deterministic!
EXPECT_EQ(config.lastWidth, 1920);
}Benefits:
- ✅ No arbitrary timeouts
- ✅ Works on slow/fast machines
- ✅ Deterministic behavior
- ✅ Faster test execution
The batching mechanism uses:
std::set<std::string>for pending changes (automatic deduplication)- Batch depth counter for nested scope support
- Mutex protection for thread-safe queue access
Changes are queued by full state path:
config.getState("width").value(1024); // Queues "This.0x12345.width"
config.getState("width").value(1920); // Replaces (same key in set)The state_lock_scope<T> template provides RAII-based exclusive access coordination for state objects. When a thread acquires a lock on a state object, other threads attempting to acquire the same lock will block until it's released.
Important: This is a cooperative coordination mechanism, not enforced access control. Direct state modifications without acquiring the lock are still possible. It's designed for threads that need to coordinate exclusive access to a state object.
Multiple threads modifying the same state object can lead to:
- Interleaved modifications: Thread A's multi-step update interrupted by Thread B
- Race conditions: Unpredictable ordering of state changes
- Inconsistent state: Partial updates visible to other threads
Example problematic scenario:
// Thread 1: Multi-step configuration
config.getState("width").value(1920);
config.getState("height").value(1080);
config.getState("aspect_ratio").value(16.0/9.0);
// Thread 2: Concurrent modification
config.getState("width").value(1024); // Interleaves!
// Result: Inconsistent state (1024x1080 with 16:9 ratio)Use RAII to coordinate exclusive access:
void Thread1() {
state_lock_scope<ConfigurationObject> lock(config);
// Exclusive access - Thread 2 will block
config.getState("width").value(1920);
config.getState("height").value(1080);
config.getState("aspect_ratio").value(16.0/9.0);
} // Lock released here
void Thread2() {
state_lock_scope<ConfigurationObject> lock(config);
// Blocks until Thread1 releases lock
config.getState("width").value(1024);
config.getState("height").value(768);
}template<class This>
class state_lock_scope
{
public:
// Constructor - acquires lock
explicit state_lock_scope(state_object<This>& obj);
// Destructor - releases lock automatically
~state_lock_scope();
// Manual unlock (can't re-lock)
void unlock();
// Non-copyable, non-movable
state_lock_scope(const state_lock_scope&) = delete;
state_lock_scope& operator=(const state_lock_scope&) = delete;
};Pattern 1: Basic Exclusive Access
Coordinate multi-step state updates:
void updateConfiguration(ConfigurationObject& config, int w, int h) {
state_lock_scope<ConfigurationObject> lock(config);
config.getState("width").value(w);
config.getState("height").value(h);
config.getState("aspect_ratio").value(static_cast<double>(w) / h);
config.getState("last_update").value(getCurrentTime());
}Pattern 2: Critical Sections
Protect read-modify-write sequences:
void incrementCounter(ConfigurationObject& config) {
state_lock_scope<ConfigurationObject> lock(config);
int current = config.getState("counter").value<int>();
config.getState("counter").value(current + 1);
}Pattern 3: Early Unlock
Release lock before expensive operations:
void processWithLock(ConfigurationObject& config) {
state_lock_scope<ConfigurationObject> lock(config);
// Critical section
std::string data = config.getState("data").value();
config.getState("status").value("processing");
lock.unlock(); // Release early
// Non-critical work (no lock needed)
std::string result = expensiveComputation(data);
// Need lock again? Create new scope
{
state_lock_scope<ConfigurationObject> lock2(config);
config.getState("result").value(result);
}
}Pattern 4: Combining with Batching
Use both batching and locking for optimal performance:
void bulkUpdate(ConfigurationObject& config, std::vector<int> values) {
state_lock_scope<ConfigurationObject> lock(config);
state_change_batch_scope<ConfigurationObject> batch(config);
// Exclusive access + deduplication
for (int val : values) {
config.getState("value").value(val);
config.getState("timestamp").value(getTime());
}
} // Batch flushes, then lock releasesImportant: Locking a child state does NOT block access to parent state objects.
ConfigurationObject config;
StateObject<int>& childState = config.getState("child");
// Thread 1: Locks child
state_lock_scope<StateObject<int>> lock(childState);
childState.value(42);
// Thread 2: Can still access parent (NOT blocked)
config.getState("other_child").value(100); // ✅ Works fineThis design allows fine-grained locking without blocking unrelated state modifications.
state_lock_scope is a coordination primitive, not an enforcement mechanism:
✅ What it does:
- Blocks other threads from acquiring the lock
- Provides mutual exclusion between cooperating threads
- Guarantees serialized access for threads using locks
❌ What it doesn't do:
- Prevent direct state access without a lock
- Enforce access control at the API level
- Block
getState()orvalue()calls from other threads
Example:
// Thread 1: Holds lock
state_lock_scope<ConfigurationObject> lock(config);
config.getState("width").value(1920);
// Thread 2: Also uses lock - BLOCKS
state_lock_scope<ConfigurationObject> lock2(config); // Waits for Thread 1
// Thread 3: Direct access - NOT BLOCKED
config.getState("height").value(1080); // ⚠️ No coordinationDesign rationale: Enforcing locks in getState() caused deadlocks due to signal callbacks and re-entrant patterns. The coordination model provides synchronization for threads that need it while maintaining flexibility.
- Lock acquisition: Thread-safe via internal mutex
- Lock release: Automatic in destructor (exception-safe)
- Multiple locks: Different state objects can be locked independently
- Nested scopes: Same thread can nest locks (but consider refactoring)
- Keep critical sections short: Hold locks only as long as needed
- Avoid blocking operations: Don't do I/O or sleep while holding lock
- Document locking requirements: Comment which functions expect locks
- Consistent locking: If one thread uses lock, all should
- Combine with batching: Use both for complex multi-state updates
- Consider deadlock: Don't acquire multiple locks without careful ordering
Verify exclusive access:
TEST(StateTest, ExclusiveAccess) {
ConfigurationObject config;
std::atomic<bool> holder_finished(false);
boost::thread t1([&]() {
state_lock_scope<ConfigurationObject> lock(config);
boost::this_thread::sleep_for(boost::chrono::milliseconds(50));
holder_finished.store(true);
});
boost::thread t2([&]() {
boost::this_thread::sleep_for(boost::chrono::milliseconds(10));
state_lock_scope<ConfigurationObject> lock(config);
EXPECT_TRUE(holder_finished.load()); // t1 must be done
});
t1.join();
t2.join();
}Use waitForHandlers() for deterministic tests:
TEST(StateTest, LockWithWait) {
ConfigurationObject config;
{
state_lock_scope<ConfigurationObject> lock(config);
config.getState("value").value(42);
}
config.waitForHandlers(); // Wait for signal handlers
EXPECT_EQ(config.lastValue, 42);
}// Per-app root accessor (preferred call-site spelling)
namespace cvc {
state& app::root(); // member shorthand
state& state::instance(app& ctx); // free-form equivalent
}
// Then use the functor form for child paths:
app.root()("level1.level2.key");
cvc::state::instance(app)("level1.level2.key");
// Direct construction
state(const std::string& name = std::string());
// Copy semantics (shallow copy - same node)
state(const state& s);
state& operator=(const state& s);// Get raw string value
std::string value() const;
// Get typed value
template<class T>
T value() const;
// Get value with callback
template<class T>
T value(const boost::function<void(T)>& callback);
// Set value
template<class T>
void value(const T& val);
// Wait for value (futures API)
template<class T>
T wait_for_value(boost::chrono::milliseconds timeout = boost::chrono::milliseconds(0));
// Get value future
template<class T>
state_future<T> value_future();
// Check if initialized
bool initialized() const;// Get data (internal boost::any)
boost::any data() const;
// Get typed data (throws type_conversion_error on mismatch)
template<class T>
T data();
// Get data with callback
template<class T>
T data(const boost::function<void(T)>& callback);
// Set data
void data(const boost::any& d);
// Wait for data (futures API)
template<class T>
T wait_for_data(boost::chrono::milliseconds timeout = boost::chrono::milliseconds(0));
// Check if data exists
bool has_data() const;// Get property value
std::string property(const std::string& key) const;
// Set property
void property(const std::string& key, const std::string& val);
// Check if property exists
bool has_property(const std::string& key) const;
// Get all property keys
std::vector<std::string> property_keys() const;// Get child node
state child(const std::string& name);
// Get parent node
state parent();
// Get node name
std::string name() const;
// Get full path
std::string path() const;
// Get all children
std::vector<state> children();The futures API provides async/await-style functionality:
wait_for_value<T>(timeout)- Block until value is set (with optional timeout)wait_for_data<T>(timeout)- Block until data is set (with optional timeout)value_future<T>()- Get future object for advanced control withis_ready(),wait_for(),get(),get_for()value<T>(callback)- Get value and register callback for changesdata<T>(callback)- Get data and register callback for changes
See the Futures API section above for detailed examples and patterns.
// Export to JSON string
std::string dump() const;
// Import from JSON string
void read(const std::string& json);void loadConfig() {
// Set default configuration
state::instance(app)("app.window.width").value(1920);
state::instance(app)("app.window.height").value(1080);
state::instance(app)("app.theme").value("dark");
// Add metadata
state::instance(app)("app.window.width").property("min", "640");
state::instance(app)("app.window.width").property("max", "3840");
// Monitor changes
state::instance(app)("app.theme").valueChanged.connect([](std::string theme) {
applyTheme(theme);
});
}
void saveConfig() {
std::string json = state::instance(app)("app").dump();
writeFile("config.json", json);
}
void restoreConfig() {
std::string json = readFile("config.json");
state::instance(app)("app").read(json);
}// Stage 1: Load data
void loadStage() {
cvc::volume vol("input.rawiv");
state::instance(app)("pipeline.input").data(vol);
}
// Stage 2: Process (waits for stage 1)
void processStage() {
cvc::volume input = state::instance(app)("pipeline.input")
.wait_for_data<cvc::volume>(boost::chrono::seconds(30));
input.bilateral_filter(5.0, 0.1);
state::instance(app)("pipeline.filtered").data(input);
}
// Stage 3: Output (waits for stage 2)
void outputStage() {
cvc::volume result = state::instance(app)("pipeline.filtered")
.wait_for_data<cvc::volume>(boost::chrono::seconds(30));
result.write("output.rawiv");
state::instance(app)("pipeline.complete").value(true);
}class TemperatureMonitor {
std::atomic<bool> running_{true};
boost::thread monitor_thread_;
public:
TemperatureMonitor() {
// Set up monitoring with callback
state::instance(app)("sensor.temperature").value<double>([this](double temp) {
if (temp > 100.0) {
std::cerr << "ALERT: High temperature: " << temp << std::endl;
state::instance(app)("alarm.triggered").value(true);
}
});
// Monitoring thread
monitor_thread_ = boost::thread([this]() {
while (running_) {
try {
// Wait for temperature reading
double temp = state::instance(app)("sensor.reading")
.wait_for_value<double>(boost::chrono::seconds(1));
// Update display
state::instance(app)("sensor.temperature").value(temp);
} catch (const cvc::timeout_error&) {
// No reading yet, keep waiting
}
}
});
}
~TemperatureMonitor() {
running_ = false;
monitor_thread_.join();
}
};class AsyncServer {
std::atomic<bool> running_{true};
boost::thread server_thread_;
public:
AsyncServer() {
server_thread_ = boost::thread([this]() {
while (running_) {
try {
// Wait for request
std::string request = state::instance(app)("server.request")
.wait_for_value<std::string>(boost::chrono::seconds(1));
// Process and respond
std::string response = processRequest(request);
state::instance(app)("server.response").value(response);
} catch (const cvc::timeout_error&) {
// No request, keep waiting
}
}
});
}
std::string makeRequest(const std::string& req) {
state::instance(app)("server.request").value(req);
return state::instance(app)("server.response")
.wait_for_value<std::string>(boost::chrono::seconds(10));
}
};// Setting an int value
state::instance(app)("number").value(42);
// Attempting wrong type conversion throws type_conversion_error
try {
// This will throw because "42" cannot be cast to Config
Config cfg = state::instance(app)("number").data<Config>();
} catch (const cvc::type_conversion_error& e) {
std::cerr << "Conversion failed: " << e.what() << std::endl;
// Output: "Conversion failed: cvc::type_conversion_error exception:
// Failed to cast data to requested type: ..."
}// Wait with timeout
try {
int value = state::instance(app)("slow.computation")
.wait_for_value<int>(boost::chrono::milliseconds(100));
} catch (const cvc::timeout_error& e) {
std::cerr << "Timeout: " << e.what() << std::endl;
// Output: "Timeout: cvc::timeout_error exception:
// Timeout waiting for value at path 'slow.computation'"
}cvc::exception (inherits from boost::exception)
├── cvc::type_conversion_error // Data/value type mismatches
├── cvc::timeout_error // Futures timeout exceeded
├── cvc::read_error // File I/O errors
└── ... (other CVC exceptions)- Values: Stored as strings, involve conversion overhead
- Data: Stored as
boost::any, zero conversion for exact type matches - Use values for simple types (int, double, string)
- Use data for complex objects (geometry, volumes, custom structs)
- Callbacks execute in the thread that modifies the state
- Keep callbacks short and non-blocking
- For expensive operations, trigger async work instead:
state::instance(app)("trigger").valueChanged.connect([](std::string val) {
// Launch async processing instead of blocking
boost::thread([val]() {
expensiveOperation(val);
}).detach();
});- All operations use mutex locks (no lock-free operations)
- Futures use condition variables for efficient blocking
- Avoid holding state references across long operations
- State nodes use
boost::shared_ptr- automatic cleanup - Data objects stored in
boost::any- copy semantics - Large objects should use shared_ptr internally:
// Efficient: Shares data via shared_ptr
struct VolumeRef {
boost::shared_ptr<cvc::volume> vol;
};
state::instance(app)("volume").data(VolumeRef{vol_ptr});The state API includes comprehensive test coverage:
- 128 State Tests (100% passing)
- Basic value get/set operations
- Data get/set with type conversion
- Property management
- Tree navigation and hierarchy
- Signals and callbacks
- Futures API (11 dedicated tests)
- Thread safety and concurrent access
- Serialization (JSON dump/read)
From TESTING_COVERAGE.md:
- state.cpp: 92.75% line coverage (243/262 lines)
- state.h: 94.7% function coverage (355/375 functions)
// Thread safety test
TEST(StateTest, ConcurrentAccess) {
std::vector<boost::thread> threads;
for (int i = 0; i < 10; i++) {
threads.emplace_back([i]() {
state::instance(app)("concurrent.value").value(i);
int val = state::instance(app)("concurrent.value").value<int>();
});
}
for (auto& t : threads) t.join();
}
// Futures test
TEST(StateTest, FuturesBlocking) {
boost::thread producer([]() {
boost::this_thread::sleep_for(boost::chrono::milliseconds(100));
state::instance(app)("future.value").value(42);
});
int result = state::instance(app)("future.value").wait_for_value<int>();
EXPECT_EQ(42, result);
producer.join();
}
// Type conversion error test
TEST(StateTest, TypeConversionError) {
state::instance(app)("test").value(42);
EXPECT_THROW(
state::instance(app)("test").data<std::vector<int>>(),
cvc::type_conversion_error
);
}# Run all state tests
./build/bin/state_test
# Run specific test
./build/bin/state_test --gtest_filter=StateTest.TypeConversionError
# Run futures tests
./build/bin/state_test --gtest_filter=*Futures*- ../README.md - Project overview and quick start
- TESTING_COVERAGE.md - Test coverage metrics