Creating arrays/vectors of components. Copy/Move semantics

C++ beginner here.

I created a “Drumpad” class, which is essentially just a component onto which I can drag and drop samples, which then are added to a sampler as a sound.

My idea was to write the class for one pad, and than create something like a “padmatrix” class. This class would simply “have” 127 pads - each of these pads then would automatically correspond to a midi note being played etc. (basically like in abletons drum rack for example).

However when I try to create a vector

std::vector pads;
const int NUM_PADS{127};

as a private member in the padmatrix class and then in the constructor do something like:

for(int i = 0; i < NUM_PADS; i++)
pads.push_back(Pad(stuffToInitPad,…));

it won´t compile. And here is where my C++ knowledge is challenged heavily. The error thrown says that the copy constructor (I think) of Pad was deleted. The deleted function looks like this:
Pad::Pad(const Pad&) → which I think is the copy constructor.
This would make sense, because my pad class has the default created
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Pad)
macro in it.

But isnt something without a name like push_back(Pad(stuffToInitPad,…)); a temporary rvalue?
Commenting out the makro and adding a default copy and move constructor did not help… but I´m not sure if I did it correctly.

What is the correct way to add objects to containers? What do my classes need/ what are they not allowed to be/have?

I really think just manually writing down 127 pads is not the right way to go. Especially since I know that this class still has to change a lot.

https://en.cppreference.com/w/cpp/language/move_constructor

The explicitly deleted copy constructor counts as user declared, so there’s no implicitly declared move constructor, and as there’s no user declared move constructor either, there’s none at all. In short, Components can’t be copied nor moved. If you need a bunch of them, use a vector of unique pointers.

The object you pass to push_back, either temporary or not, lives in some place in memory. The vector data lives in some other place, so push_back can’t use the object at its original location, it needs to recreate it. The difference between copying and moving only matters when part of the object’s data is stored outside of its own memory. For example, a vector object doesn’t contain its data, which is stored somewhere else. When a vector is copied, not only the vector object itself is copied, but also the data it points to. When a vector is moved, the vector object is still copied, but the data is not -the new object points to the same data, and the original vector is left empty. Components can’t be copied or moved because pointers to them are kept by the GUI system, and updating all those pointers on every move is a needless hassle. But you can still move unique_ptrs to them, so in your case you can do

std::vector<std::unique_ptr<Pad>> pads;
pads.emplace_back (new Pad (args));

Ok so I somehow made it work, but I´m not sure at all if this is the correct way of doing it:
In the matrix private section:

SamplerBaseAudioProcessor& audioProcessor;
const int NUM_PADS{ 3 };
Pad pad0{ audioProcessor, “Pad 0”, 0 };
Pad pad1{ audioProcessor, “Pad 1”, 1 };
std::vector<std::unique_ptr> pads;

And then inside the constructor for each pointer individually:

std::unique_ptr pad0Pointer;
pad0Pointer.reset(&pad0);
pads.push_back(std::move(pad0Pointer));

std::move finally made it compile, but I´m not sure exactly what is going on. And I´m sure there must be a more “automated” way to create these things (with a loop).
Otherwise I will have to copy and paste everything 127 times?

I guess you did this right if it compiled, but just in case: unique_ptr is a class template, not a class by itself. Each instantiation (unique_ptr<int>, unique_ptr<Pad>) is a separate class. You can’t have a vector of "unique_ptr"s, they have to be unique_ptrs of something: std::vector<std::unique_ptr<Pad>>.

I don’t quite follow what you did there, but you can fill the whole vector like

for (int i = 0; i < NUM_PADS; ++i)
    pads.emplace_back (new Pad (audioProcessor, "Pad " + juce::String (i), i));

(assuming the second argument is a juce::String.)

1 Like

Sorry, somehow the forum did not show me the second part of your answer, which is a shame.
So because pads only can hold unique pointers to a pad, this creates a new Pad on the heap which is pointed to via a unique pointer?
I just realized in my reply above I created the pad objects on the stack, which is probably super dumb anyways… Because to my knowledge the stack is not super large, and if I were to create a PadMatrix object 127 pads would lay around on the stack until their “wrapper class”, the PadMatrix itself is deleted, am I right?

Yeah this makes much more sense. Sorry, as I said I´m a beginner, so I´m just breaking things^^

Your last code snipped is exactly what I was looking for!

1 Like

Yup.

The local Pad objects would live until their scope (the braces that enclose their declaration) ends, then they would be destroyed, and your unique_ptrs would be broken. You never make a unique_ptr to a local object -a unique_ptr owns its content, so it manages its lifetime. A local object already has a defined lifetime which unique_ptr can’t overrule -it can’t be owned by anything else than its scope.

Edit -Sorry, I just realized they were not local, they were members of PadMatrix. So yup, they would live until PadMatrix is destroyed -of course you already noticed that having 127 separate members and then a vector of pointers to them doesn’t make a lot of sense :sweat_smile: The thing about ownership still holds though -if you make unique_ptrs to members, unique_ptr’s destructor will try to delete them. Those objects are already owned by PadMatrix, so unique_ptr can’t own them. That’s why it’s called “unique” -if the object is owned by something else, you’ll get a crash.

1 Like

Ah man. I actually should have known that. It´s the first time I actually “just write code” in C++, so stupid errors are made I guess and it´s hard to implement these things somehow.

Thank you ver much for the detailed clarification!

1 Like

We two certainly are the masters of cross commenting.
So I tried it out. And for now, it all seems to work. The code needs a hell lot of cleaning but I´ll get there.

So just to make sure if I get it right, at least this is how I think it works:
If the pads vector is created as a private stack member, it will live until the object itself, padMatrix, gets deleted.
Furthermore
pads.emplace_back(new NoY_Pad(audioProcessor, "Pad " + juce::String(i), i));
both creates a new Pad object on the heap AND a pointer to it. But we don´t need to manually delete anything here, because the vecor points to it via a unique pointer. So if the vector gets deleted, the individual “pad indexes” get savely deleted as well.
With your code above there is no need to copy or move anything, since the new Pad object is created and initialized and pointed to “directly” into/by the vector.
I hope I kind of got that right. If not, feel free to roast me.

I am also able to do things like
pads.at(0).get()->setBounds(getWidth() / 2 - 50, getHeight() / 2 - 50, 100, 100);
and believe it or not, the pad corresponds to midiNoteNumber 0. :smile:

That sounds right. To be more precise, new Pad (args) creates the object on the heap, returning the pointer. This is passed as an argument to emplace_back, which creates “in place” at the end of the vector a unique_ptr<Pad> initialized with the pointer returned by new. When PadMatrix is destroyed, the pads member is destroyed, vector's destructor destroys its elements, and unique_ptr's destructor invokes Pad's. The ownership goes PadMatrix->vector->unique_ptr->Pad. If the use of new seems frightening, instead of pads.emplace_back (new Pad (args)) you can write pads.push_back (std::make_unique<Pad> (args)), where make_unique creates both the Pad object and the unique_ptr, and passes this to push_back, which moves it to the vector.

pads.at(0).get()->setBounds

aka pads[0]->setBounds :relaxed:

Whether it’s private or not. Also, a member is not “on the stack” by itself -generally speaking, locals are created on the stack and new creates on the heap. So a member is wherever its owner is -if the owner is a local or part of a local, on the stack; if it’s created by new or part of something created by new, on the heap.

1 Like

Ah ok that makes much more sense. Wow I got way more knowledge out of this question than expected. Thank you very much kamedin!

1 Like

Heres a nice video of the use of emplace_back:

Also, as you’re new to C++. Check out the C++ series.

You can also use juce::OwnedArray<Pad> instead of std::vector<unique_ptr<Pad>>
The semantics are a bit easier to use/understand, though I don’t know if using OwnedArray is frowned upon at this point in JUCE’s life.

https://docs.juce.com/master/classOwnedArray.html

1 Like

Certainly not frowned upon, in fact JUCE’s array classes almost always beat std::vector in performance so juce::OwnedArray would be a smart choice.

Would be great having a benchmark or some context of that.

From what I know there are so many factors (arch, platform, compiler, use-case) there’s no absolute answer iiuc.

1 Like

I’m sure there’s some cases where the STL is far better, and I’ve never really tested it myself. All I know is that there have been attempts to replace juce::Array with std::vector but it was found to be significantly slower for some crucial tasks.

There’s a few comments about it in this thread from this comment onwards:

TLDR, juce::Path is one of the cases where replacing juce::Array with std::vector is much slower.

Having said that, there’s also this comment from Jules recommending the use of STL containers where possible, although that’s older than the previous thread so perhaps the performance difference wasn’t known at the time:

Back then when we were at Roli @t0m gave a talk about the performance of juce::Array, maybe the slides are still there and could be shared?

I’ve not kept the slides, but the main difference is that juce::Array uses uses realloc to change capacity. The allocation interface in std::vector doesn’t allow you to do this, so can be slower.

Ironically I found the documentation for the classes Owned/ReferenceCountedArray yesterday myself, but I had some confusion there.
A juce::Synthesiser´s sounds are being stored in a ReferenceCountedArray and can be returned via getVoice(int index) etc., but this seems a bit weird:
When the sounds are no longer owned the array deletes the entry (no surprise since its reference counted) and then the whole Array seems to be “rearranged”, meaning if index 0 is deleted, index 1 becomes 0, 2 ->1, 3->2 etc…
This makes sense from a data structure perspective but in that use case, with trying to keep track of your sounds, especially when they´re added via a ui drag and drop is really hard.
But maybe I´m just getting something completely wrong here once more haha

I don’t know about the Synthesiser issue, but I’ll second using OwnedArray instead of vector<unique_ptr>. I’ve not been using it myself, but now that I look, it uses less memory, and the interface is better. You can say add (new T()) as you said emplace_back (new T()), but you can add many elements at once, and the ownership is embedded in the container, so it doesn’t need to construct anything.

1 Like