Using complex data type / class instance with juce::Value / juce::Var

Hello,
I have a class which has a juce::Value member. Methods of this class mutate this value, and other classes listen for changes to that value by implementing the juce::Value::Listener interface.

This works well when the juce::Value wraps a primitive data-type, but it does not work out-of-the-box when attempting to wrap class instances- or complex data types in a juce::Value, as there is “no suitable user-defined conversion” to juce::var.

What I would like to do is to do something like this:

void SomeClass::someMethodThatTriggersListeners()
{
    const auto newValue = std::pair<int, int>(1, 2); // For example
    this->value.setValue(newValue) // Error: no suitable user-defined conversion from "const std::pair<int, int>" to "const juce::var" exists
}

The fact that this doesn’t work does not surprise me, as juce::var is intended for primitives. But, I am struggling with trying to remedy this.

I did take a look at juce::Value::ValueSource - the docs have this to say:

    Used internally by the Value class as the base class for its shared value objects.

    The Value class is essentially a reference-counted pointer to a shared instance
    of a ValueSource object. If you're feeling adventurous, you can create your own custom
    ValueSource classes to allow Value objects to represent your own custom data items.

Which suggests it’s possible to do what I’m trying to achieve, though I’m not sure if this is the right approach, and also I could not find any documentation, tutorials or examples of how to properly implement the ValueSource interface.

I did find a few threads on this forum that are adjacent to my issue, but they seem to be more concerned with ValueTrees or dumping information to a JSON file. Note: I am not interested in serializing data to JSON or XML or anything like that. I just want something like a juce::Value that wraps around a complex data type that can broadcast an onChange signal to potential listeners.

The only way I’m aware to do it with var is to use an object that inherits from ReferenceCountedObject, or managed by a ReferenceCountedObjectPtr.

Otherwise var is only created for those primitive types (and a few other ones like Array and MemoryBlock).

If all you want is something to send change messages, you could use
https://docs.juce.com/develop/classChangeBroadcaster.html

Or, if there is async stuff going on…

Or, if that looks a little heavy, you could implement Listener functionality yourself:
https://docs.juce.com/develop/classListenerList.html

Or, the simplest case, if you just have one callback you want to fire when there is a change, you can add a std::function member to your class which other classes can populate with a lambda/function. Your class then calls the function when it’s updating. I think there is a class for this kind of business too, but it might be easier to just check for null yourself:
https://docs.juce.com/develop/structNullCheckedInvocation.html#details

Thank you both for your feedback. I ended up going with the ChangeBroadcaster / ChangeListener API, and that seems to do exactly what I want.

For anyone who’s curious, this is what I implemented - it’s maybe a bit heavy.

A ComplexValue is an object that wraps around some arbitrary data/structure that you want to listen to for changes:

template<typename T>
class ComplexValue : public juce::ChangeBroadcaster
{
public:
    ComplexValue()
    {
        this->value = T();
    }

    void setValue(T value)
    {
        this->value = value;
        this->sendChangeMessage(); // Prompts listeners to retrieve latest state from ComplexValue
    }

    T getValue() const
    {
        return this->value;
    }

private:
    T value;
};

A ComplexValueListener takes a ComplexValue to listen to for changes, and a callback to trigger when changes occur:

template<typename T>
class ComplexValueListener : public juce::ChangeListener
{
public:
    using Callback = const std::function<void(ComplexValue<T>&)>;
    ComplexValueListener(ComplexValue<T>& value, const Callback& callback)
        : value(value),
        callback(callback)
    {
        this->value.addChangeListener(this);
    }

    ~ComplexValueListener() override
    {
        this->value.removeChangeListener(this);
    }

    void changeListenerCallback(juce::ChangeBroadcaster* source) override
    {
        this->callback(this->value);
    }

private:
    ComplexValue<T>& value;
    Callback callback;
};

Pseudo-code example useage:

using Pair = std::pair<int, int>;
ComplexValue<Pair> value;

auto listener = ComplexValueListener<Pair>(value, [](const ComplexValue<Pair>& value)
    {
        Pair pair = value.getValue();
        someObjectThatCares.onPairChanged(pair);
    }
);

// Then, sometime later...
value.setValue(Pair(1, 2)); // Triggers listener callback