Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

push_back/emplace_back a shallow copy of an object into another vector

Tags:

c++

python

vector

Say I have the following code

class Car {
    public:
        string color;
        string name;
        Car(string c, string n): color(c), name(n){}            
}

int main() {
    vector<Car> collection1;
    vector<Car> collection2;
    collection1.emplace_back("black", "Ford");
    collection1.emplace_back("white", "BMW");
    collection1.emplace_back("yellow", "Audi");

    //Question comes here
    collection2.push_back(collection1[0]);

}

Now I believe this does a deep copy of collection1[0]. I have tried using collection2.emplace_back(move(collection1[0])), but then the data fields of collection1[0] would be gone. I just want this "black Ford" to exist in both vectors, and changes made to this particular object, through either vector, would be reflected on both vectors.

I am guessing that for a vector of real objects, the elements of this vector take actual memory. So an element of collection1 must be independent on any element of collection2. I think the easiest way is to let collection1 and collection2 to be vectors of pointers and point to the same vector of Car. But is there any possible means to make the above code work, without using vector of pointers. I ultimately want to return both of these collections back to the previous function, so making vectors of pointers is meaningless.

In short, I want to simulate the List.append() method in python.

collection1 = [Car("black", "Ford"),Car("white", "BMW"),Car("yellow", "Audi")]
collection2 = []
collection2.append(collection1[0])
collection2[0].color = "blue" // This affects collection1 as well
like image 697
Jraxon Avatar asked Jan 05 '17 06:01

Jraxon


2 Answers

In C++ languages, standard collections actually contain the object while in other languages like Python or Java, they actually contain references (or pointers) to objects that are stored elsewhere. But as C++ does not include garbage collection, the lifetime of the object must be explicitly managed elsewhere.

The consequence of that design is that, to allow the same object to be used in two different collections, you must use collections of pointers or references (beware, C++ does not directly allow collections of references; however, std::ref was created for that).

Depending on your use case, you could either use raw pointers (if the lifetime of the actual objects is already managed), or use smart pointers (here, std::shared_ptr) that internally manage a reference count to ensure that the object is automatically destroyed when the last shared_ptr is destroyed. This is not far from Python's references to objects, provided you are aware that the destruction of the last shared_ptr will actually destroy the object(*). Said differently, do not keep any other pointers or references to it if you do not want it to become dangling.

Alternatively, if the collections are not symmetric—that is, if one will actually contain all the objects, while the other will only contain references to objects from the former one—references will be your best bet, and the second collection could be a std::vector<std::reference_wrapper<Car>>.


Addition per MvG comment.

There is a possibly annoying difference between Python objects and C++ shared_ptr. Python has a full garbage collector that is clever enough to detect circular references and destroys the cycle as soon as there are no external references. Example:

>>> b = ['x']
>>> a = ['y']
>>> b.append(a)
>>> a.append(b)
>>> a
['y', ['x', [...]]]
>>> b
['x', ['y', [...]]]

a contains a ref to b which contains a ref to a...

If a is deleted (or goes out of scope) b will still contain the full chain

>>> del a
>>> b
['x', ['y', [...]]]

but if both a and b are delete (or go out of scope), the gc will detect that there is no more external ref and will destroy everything.

Unfortunately, if you manage to build a cycle of C++ objects using std::shared_ptr as it only uses local ref counting, each object will have a ref to the other and they will never be deleted even when they will go out of scope which will lead to a memory leak. An example of that:

struct Node {
    int val;
    std::shared_ptr<Node> next;
};

a = make_shared<Node>();  // ref count 1
b = make_shared<Node>();
a.next = std::shared_ptr<Node>(b);
b.next = std::shared_ptr<Node>(a); // ref count 2!

Hell has come here: even when a and b will both go out of scope, the ref count will still be one and the shared pointers will never delete their objects, what should have normally happend without the circular reference. The programmer must explicitely deals with that and break the cycle (and forbid it to happen). For example b.next = make_shared<Node>(); before b goes out of scope would be enough.

like image 86
Serge Ballesta Avatar answered Nov 19 '22 06:11

Serge Ballesta


Since you mentioned you don't like pointers, you can use references, but vectors can't store references (because they aren't copyable and assignable). However std::reference_wrapper wraps a reference in copyable and assignable object.

std::reference_wrapper is a class template that wraps a reference in a copyable, assignable object. It is frequently used as a mechanism to store references inside standard containers (like std::vector) which cannot normally hold references.

source: http://en.cppreference.com/w/cpp/utility/functional/reference_wrapper

vector<Car> collection1;
collection1.emplace_back("black", "Ford");
collection1.emplace_back("white", "BMW");
collection1.emplace_back("yellow", "Audi");

vector<std::reference_wrapper<Car>> collection2{collection1.begin(),
                                                collection1.end()};

Using this way, collection2 refers to the same objects as collection1 does. For example:

collection1[0].name = "frogatto!";
std::cout << collection2[0].get().name;
// prints 'frogatto!'

Important:

Note that using this way is discouraged since you must have another entity that manages insertion and removal to/from collection1 and takes appropriate actions on collection2. @Serge Ballesta's answer is better than mine. Use std::shared_ptr. Try to love and embrace pointers :)

like image 5
frogatto Avatar answered Nov 19 '22 07:11

frogatto