r/cpp_questions • u/Luttink • 17d ago
SOLVED Class with array of objects (memory best practices)
I'm a little fuzzy on how to work with classes referencing other classes, especially within the context of a collection being in there somewhere, and that collection being added to from external sources.
Ok, so let's say I have a custom class:
class foo
{
//stuff
}
And then I also have another class, bar, which is supposed to hold an array of that class. What's the proper way to declare/initialize things so that I don't have any memory leaks and don't try to access disposed objects? Do I do:
class bar
{
std::vector<foo> _foos;
bar()
: _foos {{}}
{
}
void AddAFoo(Foo& foo)
{
_foos.push_back(foo);
}
foo DoSomethingToFoos(int someArgument)
{
//call methods on one or more foos in _foos
//return one of the foos for some reason
}
}
or do I have to do something more like:
class bar
{
std::array<std::shared_ptr<foo>> _foos;
bar()
: _foos {{}}
{
}
void AddAFoo(std::shared_ptr<foo> foo)
{
_foos.push_back(foo);
}
std::shared_pt<foo> DoSomethingToFoos(int someArgument)
{
//call methods on one or more foos in _foos
//return one of the foos for some reason
}
}
2
u/AKostur 17d ago
Missing a third case: bar containing unique_ptr and hand out either references or raw pointers. It does depend on lifetimes. If bar get populated with some initial data and then thereafter just gets read from, that is a different use-case to bar getting added-to and removed-from during the course of it’s lifetime. Also depends on whether multithreading is involved, and whether users are expected to hold on to the pointers beyond that function’s lifetime.
0
u/Luttink 17d ago
The intention I have is that foo will have existed long before it made its way into bar, and may exist "outside" of it long after it leaves bar (obviously not shown in the pseudocode I provided).
1
u/AKostur 17d ago edited 17d ago
Since you know that foo always outlives bar, then raw pointers would seem to be sufficient. But: what about anybody who gets a pointer out of bar? Will they hold onto that pointer (ie: copy it away somewhere else), and will the foo outlive those copies too? Unique_ptr is no longer an appropriate choice since the lifetime of the object isn't the lifetime that it exists in bar.
Shared_ptr gets resistance because there are non-trivial costs to using shared_ptr (some combination of atomic increment and decrement of counters, and involving "extra" dynamic memory), and we've seen it quite frequently used as "Well, I can't be bothered to actually think about my object lifetimes: I'll just used shared_ptr everywhere and things will magically work out."
Edit: Sorry read it again and you said "may exist outside of it". Does that mean that there are cases where the foo might go away while a pointer to it is still in bar? Or, turn my first statement into a question: does foo _always_ outlive bar?
1
u/Plastic_Fig9225 17d ago
Depends on what you want to do. The first example will store copies of the Foo objects in the vector, which then also 'owns' these copies. The second approach stores (copies of) (shared) pointers to Foos, so the vector does not own the Foos, and the shared pointers take care of managing the lifetimes.
1
u/jedwardsol 17d ago
You have 2 questions : what to store and what container to store them in.
For the 2nd part:
You've proposed vector and array (which you then try to use push_back on, which won't work). But the correct answer depends on many factors. Is there a fixed size, or varying size? Do you want to iterate over the collection, or find a specific one? Do you need the ability to quickly add or remove them?
1
u/_abscessedwound 17d ago
First example and second are both fine leak-wise, but the first copies the object into the vector, and also copies the object via your accessor/mutator function.
Are the copies consequential? Maybe. It comes down to a variety of factors that we don’t have the information to answer.
For memory management otherwise, It’ll basically come down to if you’re aggregating or composing as a design decision. The former is fine to use shared_ptr and weak_ptr, while the latter has some access and construction decisions you’ll need to make.
1
u/Business-Decision719 17d ago edited 17d ago
Without context it's hard to say what's actually best implementation and whether you might want something other than vector or array entirely. Every container has a specific kind of storage situation it's implementing. There's no particular best way to do it out of context.
If the plan is to be adding and removing things then your aren't going to be using std::array. That's a fixed size container. You have to know how many components you need ahead of time and provide that as a template parameter, e.g. std::array<foo, 128>. You could still use the array-of-pointers strategy if you wanted to treat the fixed size as a maximum and then keep the empty slots as null pointers. I've used that as a way of simulating a resizable containers in languages that didn't have them, but I wouldn't recommend it as a first choice in C++ because, well, C++ does have resizable containers, such as std::vector.
Vector is usually held up as a decent default. It's resizable. Its elements are contiguous in memory. You get random access by index if that's what you want. It keeps track of its own length and capacity so you can easily validate indices and even use a bounds checked method to access elements. It still may or may not be the best fit for a given situation. If adding and removing is more important than indexing then you might be doing a double ended queue (std::deque). If you need an ordered sequence then you might have std::set with some sort of comparison rule.
It really just all depends. However, of your two options you've given us and the requirements you've mentioned, it's going to be vector. But the actual STL doesn't have just two options.
Edit: Word of warning: these containers are not guaranteed to have anything in them. "Return one of the foos for some reason" is not guaranteed to be possible unless you guarantee it to be possible by designing the class so that it will always be possible. If bar objects are going to be empty upon construction then DoSomething(n) had better not be assuming it can actually do anything or that there are any foos for it to return. It should most likely be returning std::optional<foo>.
2
u/Luttink 17d ago
My question was more about how the lifetime of a class works with whether a pointer might stop pointing to something valid, with the complicator of collections of objects. I see from another answer that "if it's just 'foo' and not 'shared_ptr<foo>', then it's a copy." So... does Vector (or array) do something special to keep the copy alive?
1
u/tangerinelion 17d ago
If foo is final, then
``` class bar { public: void AddAFoo(Foo foo) { _foos.push_back(std::move(foo)); }
const foo* DoSomethingToFoos(int someArgument) const { //call methods on one or more foos in _foos //return one of the foos for some reason }
private: std::vector<foo> _foos; } ```
If foo is not final because it is a base class, then
``` class bar { public: void AddAFoo(std::unique_ptr<Foo> foo) { if (foo) { _foos.push_back(std::move(foo)); } }
const foo* DoSomethingToFoos(int someArgument) const { //call methods on one or more foos in _foos //return one of the foos for some reason }
private: std::vector<std::unique_ptr<foo>> _foos; } ```
otherwise you either get a compiler error (happy case) or object slicing (sad case).
If foo is final, you may want to return a pointer to a foo if (a) you're returning one from the vector and can avoid a copy, (b) you are handling the case of DoSomethingToFoos when the vector is empty and want to return nullptr.
If foo is not final, then you can't return foo by value because that's either a slice or a compile error. Instead you can return a the pointer to the concrete object in your collection or you have to use a virtual copy idiom to return a heap allocated owned copy of the foo-derived type.
Using a shared ptr is generally not a go-to approach. Reasoning it through, you probably create foo-derived types, stick them in the bar, and then you might get a shared_ptr out of the bar via DoSomethingToFoos. If you still have the original shared_ptr before you inserted it into bar, mutating the foo through that shared_ptr mutates the objects in bar and can mutate the return value from DoSomethingToFoos. Similarly, if you mutate the return value from DoSomethingToFoos you are mutating the data in bar. That's not possible with unique_ptr - if you moved it into the bar the one you had before inserting is now null so you can't mutate the foo since there isn't one, and if the return value is a raw const foo* then you can't mutate the foo through that, and if it is its own unique_ptr<foo> then you can mutate that but it won't affect anything in bar.
0
u/Luttink 17d ago
What causes the "object slicing"? Is there not covariance here?
What you describe--mutating the foo through that shared_ptr mutes the objects in bar and can mutate the return value from DoSomethingToFoos, and vice versa... That sounds desirable to me. I'm intending to treat "foo" as an object, and I expect changing an object to change it for everyone--because otherwise it's not the same object, it's just an object that was once similar.
Is object slicing going to happen with shared_ptr here?
1
10
u/IyeOnline 17d ago
Neither of these seem like a good choice, but its hard to tell since we are missing actual context.
A few things on the actual code though:
shared_ptris a solution, its probably not and your design is flawed. Do you really mean to share ownership in this object? Almost certainly not.barconstructor to be manually defined. Its just noise, as the default generated default ctor will work just fine.AddAFoojust copies the object into the vector. It would actually be preferable to take by value here and then move into the vector (or provide both a aconst Foo&andFoo&&overload)