CachedValue not updated - how to use correctly?

So this is fairly complex and your final use case will probably depend on what trade-offs you’re willing to make.

A few things to verify before we dig in to this:

MyComponent example and CachedValues:
As you’ve found out, the order of ValueTree callbacks is fairly indeterminate (technically I think they’re called in the order they’re added in but I don’t think this is specified or to be relied upon). This means that in your first example, MyComponent::valueTreePropertyChanged is called before the CachedValue's internal valueTreePropertyChanged callback so it’s cachedValue member still has the old value.

As has already been pointed out, you can work around that by calling name .forceUpdateOfCachedValue() in updateName().
This is generally fine for UI stuff like this but probably not suitable for performance code as it kind of defeats the caching aspect of CachedValues.

For UI code, one solution to this problem is to simply update your components asynchronously. E.g. make MyComponent inherit from juce::AsyncUpdater and then in MyComponent::valueTreePropertyChanged, you’d call triggerAsyncUpdate(). Then in handleAsyncUpdate() you’d call updateName(). You now know at this point all the valueTreePropertyChanged will have been made so you can safely use the cached version of name (by effectively calling get() or using the operator overload as in the example).

Another alternative here would be to use a juce::Value to handle the callback. These can handle the async update automatically for you so your example would keep a Value member and listen to changes in that, calling in the constructor (where nameValue is a member “Value nameValue;”:

nameValue = name.getPropertyAsValue()
nameValue.addListener (this);

Constraining CachedValues
The RangedWrapper stuff I wrote a few years ago was primarily to work with the existing CachedValue APIs and to get people thinking about ways to (ab)use the type system.
Since then I’ve started using my own more flexible ConstrainedCachedValue class that is the same as CachedValue but has a void setConstrainer (std::function<Type (Type)> constrainerToUse); method that call this when returning the returning the value.

Additionally, it also constrains the current value when setting the constrainer (or setting the value) which means that it will actually update the underlying ValueTree when one is used.
This seems to be closer to your requirements. (This is here: https://github.com/Tracktion/tracktion_engine/blob/master/modules/tracktion_engine/utilities/tracktion_ConstrainedCachedValue.h)


CachedValue Callbacks
There’s been some talk here of using std::function objects as callbacks. Generally I’m all for this and use it a lot, especially for GUI components (like Slider, Button etc). However, I think the fundamental difference there is that you usually have only one thing that needs to listen to changes in the Slider/Button and that’s usually to update your model.

With CachedValues, these are your model so are usually “listened” to in at least two places (usually one control to set the value and another to respond to the change in UI, maybe another to respond trigger audio changes etc. An example of this would be a button to control the visibility of a panel). If you’re using a CachedValue from multiple places because it caches, having a single callback probably wouldn’t cut it. Maybe a CachedValue with ListenerList would be more appropriate here…

The other thing you have to remember when using shared CachedValues (e.g. non-small scoped members such as in your original example) is that you’d have to clear std::functions or remove listeners to avoid dangling listeners or captured this pointers.

I do agree in your MyExample however that a single std::function callback is probably the best approach.


Bringing it all Together
To be honest, I use ValueTree and all the ancillary classes a variety of ways depending on the use case. I still haven’t found the “perfect” way of doing all tasks.

What I would say though is that I’m definitely moving towards a more binding style rather than creating wrappers for everything. This often involved making a shim class that inherits from juce::ReferenceCountedPointer so it can be stashed in a Component::getProperties() NamedValueSet. Given your name example I think the ideal would be to be able to write something like:

CachedValueCallback::bind (myComponent, name,
                           [this] (auto& comp, auto n) { comp.setName (n); });

This would probably use something like your MagicValue internally but could be implemented in a variety of ways.

The main things you need to decide are how visible you want these to be i.e. does the rest of codebase have raw access to the underling ValueTree or does all access go through CachedValues? The thing that’s always bugged me about dynamic types like this is that there’s nothing really stopping another part of the code from setting a nonsense value. This can happen in any dynamically typed language though and there are plenty of those around…

7 Likes