Understanding ValueTrees better

I’m playing around for the first time with ValueTrees for an application and some things seem odd…obviously it could just be my lack of understanding.

For my top level ValueTree, I want to have a member variable, but…:

class MyMain
{
    
    // member variables
    ValueTree topTree;   // this is invalid
    
    ValueTree topTree(Identifier("TREE"));  // can't do this
    
    static Identifier treeID("TREE");  // can't do this
    ValueTree topTree(treeID);


    // so it seems I have to do this:
    MyMain()
    { 
        ValueTree fake(Identifier("TREE"));  // this just seems odd
        topTree = fake;
    }
}

I also don’t see a clean way to create static Identifiers. I saw @dave96’s talk in which he does this:

namespace IDs
{
#define DECLARE_ID(name) const juce::Identifier name (#name);
	
	DECLARE_ID (TREE)
	
	DECLARE_ID (saturation)
    DECLARE_ID (gain)
	
#undef DECLARE_ID
} 

Maybe it’s just me, but that seems like a workaround for something that’s not fully worked out. I mean, I’ve been trying to get away from using #defines and the like…but it does work.

So now I’m doing this:

class MyMain
{
    MyMain()
    {
        ValueTree dummy(IDs::TREE);
		tree = dummy;
    }

    ValueTree tree;
}

Sorry for the whining, but as I said, this all seems odd. Maybe it will sink it later as I use ValueTrees more, but any thoughts would help. Thanks.

did you try

class MyMain
{
    ValueTree tree{"tree"}; 
};

https://en.cppreference.com/w/cpp/language/aggregate_initialization
or

class MyMain
{
    ValueTree tree;
    MyMain() : tree("tree") { }
};

https://en.cppreference.com/w/cpp/language/data_members#Member_initialization

2 Likes

Ah…curly brackets! Yes, that worked.

Now this seems much cleaner:

namespace IDs
{
    const Identifier TREE("TREE");
    const Identifier saturation("saturation");
}

class MyMain
{
    ValueTree tree{IDs::TREE};
}

Much cleaner. Thank you.

And now I also see Dave’s clever #define trick to just type less with the same results.

Thank you.

Check out those links to understand why Curly braces, and not Parentheses.

I use a class to wrap my ValueTree’s, and thus all of the identifiers are owned by those classes, I use this format:

AudioConfig.h

class AudioConfig : public ValueTreeWrapper
{
public:
    static const Identifier AudioConfigId;
    static const Identifier DeviceNamePropertyId;
};

AudioConfig.cpp

#include "AudioConfig.h"

const Identifier AudioConfig::AudioConfigId { "Audio" };
const Identifier AudioConfig::DeviceNamePropertyId { "deviceName" };

that seems overly verbose just to get around @dave96’s #define DECLARE_ID(id) macro…

1 Like

Totally! lol… I should put together a macro, since my scoping is slightly different than his. Mostly it hasn’t been an issue, since each new wrapper starts as a copy/paste. :crazy_face:

@cpr that’s where I started as well. I still use it from time to time, when I allow the tree to be accessed from other classes and let them know the Identifiers…

Now I have the namespace IDs in the cpp, so it is nicely hidden from all other classes.

And ideally have accessor methods instead of allowing anyone to go directly into the ValueTree, that’s much better encapsulation.

@daniel I could probably could hide the Id’s as well, since, as you describe, the Wrappers actual hide all of the ‘ValueTreen-ess’ from clients using them. I have setters, getters, and std::function callbacks that the clients use.

The reason for making the IDs all in a single scope is that they’re really just wrappers around uniquely identifiable strings.

namespace A
{
   const Identifier myID { "myID" };
}

namespace B
{
   const Identifier myID { "myID" };
}

jassert (A::myID == B::myID);

So there’s really no advantage to creating the same ID in multiple scopes except maybe it’s an indication of what properties you should be setting on the tree;


I do get that if you expose your raw ValueTrees you are leaving them open to any form of mutation. (In Tracktion Engine this is actually desired as apps can store custom data in the tree which is ignored by the Engine).

This is a bit of a philosophical debate on how much protection you should give to your state and what the contract of your class is.

If I was doing some major refactoring I think I’d expose everything via type-safe, validated CachedValues (we have a ConstrainedCachedValue<T> in the Engine we’re starting to adopt) and avoid having to go via the ValueTree directly at all.

I’ll defer to your ValueTree experience, as I have only been working them for a year or so now. :slight_smile: I’m not wrapping them to prevent mutation, but instead to simplify the usage of the data model, ie. not requiring my client code to do ValueTree things. CachedValue is half the battle, but since I want to respond to changes, I also wanted a simple callback mechanism like Button::onClick.

Yeah, I get that. I’m not perfectly happy with the way we do things now but am inching closer to what I think a great API will be.

Maybe something like a CachedValue<T>::onPropertyChange callback would be good…

CachedValue&lt;T&gt;::onPropertyChange was my exact though when you mentioned CachedValue. As you said, I’m also ‘inching closer’ to something I am happy with. What I have now has made coding super quick to create new models and use them, but it needs more usage for the patterns to be seen more clearly. Regarding ValueTree mutation, I do allow access to the underlying VauleTree with getValueTree, and getValueTreeRef, accessors.

This is all starting to make me wonder about all the conversion efficiency when this scales up to a larger plugin or app. @dave96, I’m sure you know better, but it seems like there is potentially a lot of conversion from float to various intermediate types (var, Value, CachedValue, etc) when all I really need in my app is a simple float. And what happens when many variables are spread all over a large code base…how much time is spent in converting types? I haven’t looked at the underlying code but my inner optimization brain makes me think about it.

Well that’s kind of the whole point behind CachedValue. Getting the value of one of these should be as quick as the primitive it stores.

In reality, most of your data is static, most of the time. So there won’t be any costly conversions.

We do have a couple of places where we force a conversion from the String var that gets parsed from the XML to a double but those are rare cases. https://github.com/Tracktion/tracktion_engine/blob/5b281a4410c5cb6469170f1636f792e941254e4c/modules/tracktion_engine/midi/tracktion_MidiList.cpp#L17
(You don’t get this problem if you use CachedValues or store the VT as binary btw.)

Before you start optomising like this though, I’d profile to make sure it is a bottleneck.

2 Likes