Objects inside containers

Location
  1. Courses

    /

  2. Complete C++ Course

    /

  3. The C++ Standard Library

    /

  4. Containers

    /

  5. Objects inside containers

Vector of objects

Containers, like std::vector<T>, may not only hold variables of primitive types, but class objects too. To add an element at the end of a vector of objects, using the method std::vector<T>::push_back, we must give it, as argument, an object. It will then create the new element by copying the object.

Note that the destructor of the vector automatically calls the destructor of its elements.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <vector> #include <iostream> class A { public: A(int a) : _a(a) {} void print() const { std::cout << _a << "\n"; } private: int _a; }; int main() { // Defines a vector holding objects of type A. std::vector<A> vec; vec.push_back(A(7)); vec[0].print(); return 0; }

What happens in the line vec.push_back(A(7)); is that a temporary object of type A is created, then the vector increases its size, creates a new element by copying the temporary object and finally the temporary object is deleted. The result is that the constructor of A is called once (To create the temporary object), the copy constructor of A is also called once (To create the new element of the vector) and the destructor of A is called twice (First to destroy the temporary object and at the end of the program to destroy the element of the vector).

Here is an example illustrating it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <vector> #include <iostream> class A { public: A() { std::cout << "Constructor of A.\n"; } A(const A & a) { std::cout << "Copy constructor of A.\n"; } ~A() { std::cout << "Destructor of A.\n"; } }; int main() { std::vector<A> vec; vec.push_back(A()); return 0; }

With primitive types, that behavior is not problematic. However, with objects containing many member variables, that is not optimal. The C++11 version of the C++ Standard Library provides the method void std::vector<T>::emplace_back(Args&&... args) which directly creates the object at the location of the new element of the vector, by calling the constructor of the object using the arguments it received, instead of creating it by copy. We must simply give, to the method, the arguments we wish the constructor of the new element of the vector to receive.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <vector> #include <iostream> class A { public: A() { std::cout << "Constructor A().\n"; } A(int a) { std::cout << "Constructor A(int a).\n"; } A(float a, float b) { std::cout << "Constructor A(float a, float b).\n"; } A(const A & a) { std::cout << "Copy constructor of A.\n"; } ~A() { std::cout << "Destructor of A.\n"; } }; int main() { std::vector<A> vec; // To avoid reallocation. vec.reserve(3); vec.emplace_back(); vec.emplace_back(76); vec.emplace_back(14.6, 43.8); return 0; }

Reallocation of objects

The reallocation of the elements of a container holding objects is more problematic, performance-wise, than with a container holding variables of primitive types. When a vector reallocates its elements, it allocates a new memory space, copies its elements to it (If they are objects, using their copy constructor, otherwise, using the assignment operator), then, if the elements are objects, it calls the destructor of each object inside the old memory space and then free the old memory space.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <vector> #include <iostream> class A { public: A() { std::cout << "Constructor A().\n"; } A(int a) { std::cout << "Constructor A(int a).\n"; } A(float a, float b) { std::cout << "Constructor A(float a, float b).\n"; } A(const A & a) { std::cout << "Copy constructor of A.\n"; } ~A() { std::cout << "Destructor of A.\n"; } }; int main() { std::vector<A> vec; std::cout << "First emplace_back:\n"; /* Adds a new element to the vector. There is no element inside the vector, so there is no reallocation, only a memory allocation. */ vec.emplace_back(); std::cout << "\nSecond emplace_back:\n"; /* Adds a new element to the vector. The size of the memory space of the vector is only big enough to hold 1 object. There is therefore a reallocation of the object inside the vector. */ vec.emplace_back(76); std::cout << "\nThird emplace_back:\n"; /* Adds a new element to the vector. The size of the memory space of the vector is only big enough to hold 2 objects. There is therefore a reallocation of the 2 objects inside the vector. */ vec.emplace_back(14.6, 43.8); std::cout << "\n"; return 0; }

In the example above, even though we added the same elements as in the example before it, there are more instructions executed, because there has been 2 reallocations, since we did not reserve memory for the vector. The example above shows how important it is to try to avoid reallocations of objects.

The problem is even bigger when the objects inside the vector use dynamic memory allocation. In that case, since the objects are copied, the dynamically allocated memory spaces their members point to, are also copied. Here is an example showing it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <vector> #include <iostream> #include <stdlib.h> // malloc #include <string.h> // memcpy class A { public: A(unsigned arraySize) : _arraySize(arraySize) { // Allocates an array of elements of type 'float'. _array = (float*)malloc(sizeof(float) * _arraySize); } A(const A & obj) { std::cout << "The array is being copied.\n"; _arraySize = obj._arraySize; if(_arraySize == 0) _array = 0; else { // Allocates memory. _array = (float*)malloc(sizeof(float) * _arraySize); // Copies the array of the object to copy. memcpy(_array, obj._array, sizeof(float) * _arraySize); } } ~A() { free(_array); } private: float *_array; unsigned _arraySize; }; int main() { std::vector<A> vec; vec.emplace_back(100); vec.emplace_back(100); vec.emplace_back(100); vec.emplace_back(100); vec.emplace_back(100); return 0; }

As you can see, because of the reallocations, the dynamically allocated arrays pointed by the members of the objects of type A, contained inside the vector, has been copied 7 times.

In that situation, copying, in the copy constructor, the dynamically allocated array pointed by the object received in argument is not optimal, because the copied array is going to be deleted anyway (Because the object is in the old memory space of the vector). We can resolve that by simply defining the move constructor of the class A:

1
2
3
4
5
6
7
8
9
10
11
12
// Move constructor A(A && obj) noexcept { std::cout << "The array is being stolen.\n"; // Steals the array, _array = obj._array; _arraySize = obj._arraySize; // Makes the pointer point to the address 0, so the destructor of 'obj' does not delete the array. obj._array = 0; }

Now, when the memory space of a vector containing objects of type A is being reallocated, its elements are moved rather than copied.

Note that for the move constructor to be used by the vector, it must be declared/defined with the keyword noexcept. We will learn about exceptions later, but basically, that keyword is used to guarantee that this method/function does not throw exceptions.

Resizing vectors containing objects

When we resize a vector to a bigger size, new elements are created (If its size is 5 and we resize it to 7, 2 new elements are created). The method std::vector<T>::resize(size_type size, T val = T()) resizes the vector it is called from and gives its second argument to the constructor of the objects that are created. If we give a value to the second argument of the method, the constructor of the second argument receives it. The objects created by this method are constructed with their copy constructor receiving, as argument, the second argument of the method.

Note that the C++11 version of this method exist in two overloads. The main difference is that it has an overload taking only one argument, which is the size of the array, and the objects it creates are constructed using their constructor taking no argument instead of their copy constructor.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream> #include <vector> class A { public: A() { std::cout << "A()\n"; } A(int a) { std::cout << "A(" << a << ")\n"; } A(int a, int b) { std::cout << "A(int a, int b)\n"; } }; int main() { std::vector<A> vec; vec.reserve(8); std::cout << "First resize:\n"; vec.resize(3); std::cout << "Size: " << vec.size() << std::endl; std::cout << "\nSecond resize:\n"; vec.resize(5, 8); std::cout << "Size: " << vec.size() << std::endl; std::cout << "\nThird resize:\n"; vec.resize(8, 56); std::cout << "Size: " << vec.size() << std::endl; // Error, maximum of one argument for the constructor: // vec.resize(10, 56, 102); return 0; }

Here is an example showing that the objects created by the method std::vector<T>::resize are constructed by copy, when the method receives a value for its second argument:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream> #include <vector> class A { public: A(int a) { std::cout << "A(" << a << ")\n"; } A(const A & a) { std::cout << "A(const A & a)\n"; } }; int main() { std::vector<A> vec; vec.resize(5, 1); return 0; }

Destruction of the objects

When the size of a vector is reduced, the destructor of each of its elements that are now out of it is called. For example, if a vector of size 20 is reduced to 15, using the method std::vector<T>resize, its 5 last elements are destructed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream> #include <vector> class A { public: A() { std::cout << "A()\n"; } ~A() { std::cout << "~A()\n"; } }; int main() { std::cout << "Creation of a vector with 7 elements: \n"; std::vector<A> vec(7) std::cout << "\nSize reduced to 4: \n"; vec.resize(4); std::cout << "\nSize reduced to 2: \n"; vec.resize(2); std::cout << "\nDestruction of the vector:\n"; return 0; }