juce::Array< val > access from inside juce::ValueTree

Let’s say I have juce::Array< val >, which is stored as a property inside juce::ValueTree.

Now I want to have juce::Value, which is referring to that exact same property stored inside that juce::ValueTree. This I can do with Value ValueTree::getPropertyAsValue(…)

How do I modify the elements in juce::Array< val > so that they automatically get updated in the ValueTree? My issue here is that juce::Value::getValue() returns a “var”, which I assume is a copy of what’s stored inside the ValueTree, instead of referencing to it. Am I correct at assuming this?

If I am, how to work around this issue?

Can juce::CachedValue be used for this?

That is correct, but you don’t need to use getValue(). the juce::Value has a shared underlying data object, so modifications to the juce::Value are reflected in the ValueTree, but not the var returned vy getValue().

Does that help?

So is there a way for me to change Nth element in the Array which the juce::Value is referencing to?

I haven’t tried, but getProperty() returns a reference to the var object.
When you use getArray() on that var you get a pointer to the array, which should allow you to modify in place.

Just be aware of the docs reminding to not holding on to the Array pointer.

I am curious if that actually gives the benefits you hope for though.

Seems that by doing that I’ll be ignoring any use of UndoManager.

Hmm. I need to think a bit more about this.

How fast is it to return juce::var from a method?
Is that class just couple of pointers which get copied when it’s returned from a method?

I’m not sure CachedValue would work with an array. but if you need the performance improvement on your reads, you could implement a class to cache the values for you. And just use that where you need it.

I’m trying something similar at the moment. That’s when I ran into the question, if returning a juce::var (which contains an array of ints) is a fast operation or if something slow gets done in the background.

1 Like

I made the below quick test.
First I create a juce::var, which is an array of ints.
Then I return that same juce::var from a method.
I modify its content and compare it to the original juce::var.

It seems that indeed a copy is made of the juce::var, when returning it from a method. I.e. the assertion “fails”, which is the correct end result for this test.

    juce::var   m_var;

    MainComponent()
    {
        // Create our juce::var
        juce::Array<juce::var> numbers;

        for (int i = 0; i < 10; i++)
            numbers.add(i);

        m_var = juce::var(numbers);

        // Get the same var and modify it
        juce::var numbers_copy = getVar();
        numbers_copy[5] = 123;
        
        // Test if both juce::vars are identical or not (they probably shouldn't be)
        for (int i = 0; i < 10; i++)
            jassert(numbers[i] == numbers_copy[i]);
        
        // Other stuff
        setSize(600, 400);
    }
    
    juce::var getVar()
    {
        return m_var;
    }

This should mean that since a real copy is created of the juce::var when returning it from a method, it also allocates memory somehow, which is relatively slow operation in itself.

I’m afraid there are a few misunderstandings about how C++ works.

When you call this line:

m_var = juce::var(numbers);

It creates a copy. It is actually called a copy constructor. Well, numbers is not a var, but it still copies it in. You can see it because it takes a const reference to copy from. Otherwise it would be impossible for the var to write to the array afterwards.

And the method getVar() you created, returns by value, which also asks the compiler to return a copy.
If you declared it like:

juce::var& getVar()

It would not copy but return a reference to the actual m_var. But careful not to return a local variable that way.

When I write the example slightly different, it actually changes the original:

template<typename T>
void dumpArray(juce::Array<T>& array)
{
    juce::StringArray strings;
    for (const auto& i : array)
        strings.add (juce::String (i));
    
    std::cout << strings.joinIntoString(", ") << std::endl;
}

int main (int argc, char* argv[])
{
    juce::var myArray;
    
    for (int i=0; i<10; ++i)
        myArray.append (i);

    dumpArray(*myArray.getArray());
    
    myArray.getArray()->getReference(3) = 10;
    
    dumpArray(*myArray.getArray());

    return 0;
}

Caveat: myArray[3] would return a copy, a design choice that cost probably an army in dev time and counting. Always use getReference instead.

I was thinking about this a bit more and yes. Only the setProperty() and add/removeChild() as well as assigning it to a new ValueTree calls are recorded by the undo manager, which makes sense.

I tried this snippet and of course the undo() call will remove the array and we get a crash in the last dumpArray:


#include <JuceHeader.h>

template<typename T>
void dumpArray(juce::Array<T>& array)
{
    juce::StringArray strings;
    for (const auto& i : array)
        strings.add (juce::String (i));
    
    std::cout << strings.joinIntoString(", ") << std::endl;
}

int main (int argc, char* argv[])
{
    const static auto kNumbers = "numbers";
    
    juce::UndoManager undo;
    juce::var myArray;
    
    for (int i=0; i<10; ++i)
        myArray.append (i);
    
    juce::ValueTree tree ("tree");
    tree.setProperty (kNumbers, myArray, &undo);

    dumpArray (*tree.getProperty (kNumbers).getArray());

    undo.beginNewTransaction();
    if (auto* myArray = tree.getProperty (kNumbers).getArray())
        myArray->getReference (3) = 10;

    dumpArray (*tree.getProperty (kNumbers).getArray());

    undo.undo();

    dumpArray (*tree.getProperty (kNumbers).getArray());

    return 0;
}

The only way around would be to call setProperty(kNumbers, changedArray, &undo);.

My assumption was that my getVar() makes a copy (copy constructor which would create whole new instances of the contained data or something similar) of std::var. That’s why I named the variable it got stored in numbers_copy. But my comment above it simply said “Get the same var” which should have probably read “Copy the same var”.

That being said, I made a slightly modified version of the same test, which gives odd results:

    juce::var   m_var;

    MainComponent()
    {
        // Create our juce::var
        juce::Array<juce::var> numbers;

        for (int i = 0; i < 10; i++)
            numbers.add(i);

        m_var = juce::var(numbers);

        // Copy the same var, modify it
        juce::var numbers_copy = getVar();
        numbers_copy[5] = 123;

        // Print contents of both juce::vars
        for (int i = 0; i < 10; i++)
            std::cout << juce::String(m_var[i]) << ", ";
        
        std::cout << std::endl;

        for (int i = 0; i < 10; i++)
            std::cout << juce::String(numbers_copy[i]) << ", ";

        std::cout << std::endl;

        // Test if both juce::vars are identical or not (they shouldn't be)
        for (int i = 0; i < 10; i++)
            jassert(m_var[i] == numbers_copy[i]);
        
        // Other stuff
        setSize(600, 400);
    }
    
    juce::var getVar()
    {
        return m_var;
    }

The output is this:

0, 1, 2, 3, 4, 123, 6, 7, 8, 9, 
0, 1, 2, 3, 4, 123, 6, 7, 8, 9, 

Meaning, when I change numbers_copy[5], it seems that also the m_var[5] gets changed the same time. This happens regardless of me getting the std::var from getVar(), which should make a copy.

I might be missing something very obvious here, as I just woke up and I haven’t had my head cleared from the nights sleep, but at the moment something seems off here.

I think this is something I’ve ran into before which has caused me some confusion to how to use std::var and what’s really going on with them.

So I’m confused:
Is that behavior intended or not? It doesn’t feel consistent at all. I would expect, that if you get a copy of the std::var in one way, the other ways of getting the std::var would also make a copy. But the above test indicates otherwise.