Passing juce::String as const ref vs. by copy

I can’t find it now for whatever reason, but I swear I remember seeing someone somewhere with authority saying we shouldn’t bother passing juce::String by reference but rather by copy since it’s internally ref counted. However, throughout JUCE code String is passed by const reference, and very rarely by copy.

What’s the definitive answer on this?

1 Like

I would say the best advice is probably from the JUCE coding standards:

Yes, it’s ref-counted so passing a copy is cheap, but not as cheap as passing it by reference!

Overall, it probably doesn’t matter too much how you do it, as the performance implications are going to be irrelevant in 99% of cases. But if you want to get it optimal, the rules would be:

  • If the function is going to store that string somewhere, or assign it to another string, then pass it by value (and you may want to std::move out of your local parameter within the function).
  • If you’re going to store the value and it’s really performance-critical you might even want to have a two versions of the function as both a const String& and a String&&
  • If the function is just going to read the string, and if you don’t need all the String methods, then prefer to pass it as a StringRef, so that literals can also be passed in without overhead
  • If the function is just going to read the string but you do need more methods than StringRef provides, then pass it as a const String&

This could be a nice addition to the coding standards, like an asterisk in the point where passing by value/reference is mentioned

Good idea. We have an all-new coding standard doc in progress, I’ll add this to it…


Thanks @jules for that educational piece, that is very helpful!

Can you also add a bit of that information about Identifier?
I often end up converting String to Identifiers and back, for instance AudioProcessorParameterWithID, where I would have expected an Identifier, as it is often checked, and I use static Strings anyway.
Are you moving away from Identifier?

Identifiers are actually just String which have been de-duped so that any two Identifiers which are the same will share the same underlying ref-counted object. That means they’re much faster to compare, but slower to create. Otherwise, they behave as Strings do.

Ah, I always thought (or hoped), there was some trickery going on, like hash compare or similar…

Yeah, it’s actually faster than a hash compare (it’s basically just comparing two pointers), but you pay in the overhead of creating them. It’s a trade-off that makes them suitable for some uses, but not others.

I don’t follow, so a comparison is only comparing if it is the same instance? like === in JS?
That’s not the case in most of the cases, is it?

This is how I implement all my JSON/ValueTree reading logic:

static Identifier paramXY ("paramXY");
DynamicObject* object;
if (object->hasProperty (paramXY))
   foo->doSomething (object->getProperty (paramXY));

paramXY is not the same pointer as the property’s name in DynamicObject, so it will always compare the full string in O(n)?

I thought of adding a hash alongside the Identifier, so you can overload the operator==(Identifier&)
But I understand now, that is not how it works…

Not sure I understand what you mean, but the point is that any Identifier objects which contain the same text will share the same underlying string, regardless of when you created them.

Ok, I had a look into the sources, that tells more than anything…
I wasn’t aware of StringPool, right after posting I realised, that there must be something like that…

For me to take out, it only optimises (Identifier == Identifier), never a (Identifier == String).

Thanks for the clarification!

This is exactly the sort of detailed answer I was hoping for. Thanks Jules!

Looking forward to reading the new coding standards doc. Since I started learning/using JUCE when I was an intermediate C++ user several years back, the current one has had a tremendous impact on how I design and style my code.