This set of slides introduces the reader to the concept of resource wrappers, i.e., classes that are responsible for the correct handling of resources of some kind (e.g., memory). In particular, the presentation discusses the design and implementation of a simplified version of std::vector for the specific case of integer elements. In this regard, we first discuss the fundamental role of destructors as a deterministic, general-purpose undo mechanism. Second, we notice that providing an explicit destructor entails the need of a consequent explicit implementation for the copy constructor and copy assignment operator. We conclude with the formulation of the so-called "rule of three".
13. But why?
The reason is that re-implemen0ng std::vector allows us to
prac0ce many basic language facili0es at once
• Pointers & arrays
• Classes & operator overloading
14. But why?
Secondly, it allows stressing that whenever we design a class, we
must consider ini#aliza#on, copying and destruc#on1
1
For simplicity, in the remainder we are going to deliberately ignore move seman8cs, i.e., we are going to discuss
object lifecycle as of C++03
15. But why?
Finally, as computer scien2sts we need to know how to design and
implement abstrac2ons such as std::vector
16. Objec&ves
Ideally, we would like to achieve the following:
void f() {
vector_int v(7); // varying number of elements
v[0] = 7; // writing elements
std::cout << v.size(); // it knows its size
std::cout << v[1]; // values are default initialized
vector_int w = v; // copy construction
vector_int u; u = v; // assignment
// automatic memory management
// (no delete[] required)
}
18. Natural behavior
We want to get to the point where we can program using types
that provide exactly the proper4es we want based on logical needs
19. Natural behavior
To get there, we have to overcome a number of fundamental
constraints related to access to the bare machine, such as:
• An object in memory is of fixed size
• An object in memory is in one specific place
21. Designing vector_int
We start our incremental design of vector_int by considering a
very simple use:
vector_int grades(4);
grades[0] = 28;
grades[1] = 27;
grades[2] = 30:
grades[3] = 25;
22. Designing vector_int
This creates a vector_int with four elements and give those
elements the values 28, 27, 30, 25
vector_int grades(4);
grades[0] = 28;
grades[1] = 27;
grades[2] = 30:
grades[3] = 25;
23. The size of a vector_int
The number of elements of a vector_int is called its size
vector_int grades(4);
grades[0] = 28;
grades[1] = 27;
grades[2] = 30:
grades[3] = 25;
24. The size of a vector_int
Therefore, the size of grades is four
vector_int grades(4);
grades[0] = 28;
grades[1] = 27;
grades[2] = 30:
grades[3] = 25;
25. The size of a vector_int
Moreover, the number of elements of a vector_int are indexed
from 0 to size - 1
vector_int grades(4);
grades[0] = 28;
grades[1] = 27;
grades[2] = 30:
grades[3] = 25;
28. vector_int is a class
Unsurprisingly, we have to define a class and call it vector_int
class vector_int {
...
};
29. Data members
Obviously, we cannot design vector_int to have a fixed number
of elements
class vector_int {
public:
...
private:
int elem0, elem1, elem2, elem3;
};
30. Data members
Conversely, we need a data member that points to the sequence of
elements
class vector_int {
public:
...
private:
int* elem;
};
31. Data members
That is, we need a pointer member to the sequence of elements
class vector_int {
public:
...
private:
int* elem;
};
32. Data members
Furthermore, we need a data member to hold the size of the
sequence
class vector_int {
public:
...
private:
std::size_t sz;
int* elem;
};
35. Ini$aliza$on
At construc*on *me, we would like to allocate sufficient space for
the elements on the heap
// v allocates 4 ints on the heap
vector_int v(4);
36. Ini$aliza$on
Elements will be later accessed through the data member v.elem
class vector_int {
public:
...
private:
std::size_t sz;
int* elem;
};
37. Alloca&ng space
To this end, we use new in the constructor to allocate space for the
elements, and we let elem point to the address returned by new
class vector_int {
public:
vector_int(std::size_t sz): elem(...) {}
...
}
38. Alloca&ng space
To this end, we use new in the constructor to allocate space for the
elements, and we let elem point to the address returned by new
class vector_int {
public:
vector_int(std::size_t sz): elem(new int[sz]) {}
...
}
39. Storing the size
Moreover, since there is no way to recover the size of the dynamic
array from elem, we store it in sz
class vector_int {
public:
vector_int(std::size_t sz): sz(sz),
elem(new int[sz]) {}
...
};
40. Storing the size
We want users of vector_int to be able to get the number of
elements
41. size() access func)on
Hence, we provide an access func2on size() that returns the
number of elements
class vector_int {
public:
vector_int(std::size_t sz): sz(sz),
elem(new int[sz]) {}
std::size_t size() const { return sz; }
private:
std::size_t sz;
int* elem;
};
42. Sensible ini)aliza)on
In addi'on, we would like to ini'alize the newly-allocated elements
to a sensible value
vector_int v(3);
std::cout << v[0]; // output: 0 (as opposed to some random values)
43. Sensible ini)aliza)on
Again, we can do that at construc2on 2me
vector_int v(3);
std::cout << v[0]; // output: 0 (as opposed to some random values)
44. Sensible ini)aliza)on
class vector_int {
public:
vector_int(std::size_t sz): sz(sz),
elem(new int[sz]) {
for (std::size_t i = 0; i < sz; ++i)
elem[i] = 0;
}
std::size_t size() const { return sz; }
private:
std::size_t sz;
int* elem;
};
48. vector_int as a pointer
Note that – up to this point – vector_int values are just pointers
that remember the size of the pointed array
vector_int v(4);
std::cout << v.size(); // output: 4
49. Accessing elements
However, for a vector_int to be usable, we need a way to read
and write elements
vector_int v(4);
v[0] = 1; // writing elements
std::cout << v[0]; // reading elements
50. Accessing elements
That is, we would like to add the possibility of access elements
through our usual subscript nota-on
vector_int v(4);
v[0] = 1; // writing elements
std::cout << v[0]; // reading elements
51. Operator func-on
The way to get that is to define a member operator func,on
operator[]
vector_int v(4);
v[0] = 1; // writing elements
std::cout << v[0]; // reading elements
52. Defining operator[]
How can we define the operator[] overload?
class vector_int {
public:
...
??? operator[](std::size_t i) { ??? }
private:
std::size_t sz;
int* elem;
};
53. Defining operator[]
Intui&vely, one would say
class vector_int {
public:
...
int operator[](std::size_t i) { return elem[i]; }
private:
std::size_t sz;
int* elem;
};
54. Defining operator[]
Apparently, we can now manipulate elements in a vector_int
vector_int v(10);
int x = v[2];
std::cout << x; // output: 0
55. Naive implementa,on
This looks good and simple, but unfortunately it is too simple
int operator[](std::size_t i) { return elem[i]; }
58. Returning a value
Our implementa-on of operator[] returns a temporary of type
int. Hence, we cannot assign any addi-onal value to it
vector_int v(10);
v[3] = 7;
59. Returning a value
That is, le+ng the subscript operator return a value enables
reading but not wri6ng elements
int operator[](std::size_t i) { return elem[i]; }
60. Returning a reference
Returning a reference, rather than a value, from the subscript
operator solves this problem
int& operator[](std::size_t i) { return elem[i]; }
62. Constant vector_ints
Our subscript operator has s/ll a problem, namely, it cannot be
invoked for constant vector_ints
void p(vector_int const& v) {
int x = v[0];
}
63. Constant vector_ints
This is because operator[] has not been marked as const
void p(vector_int const& v) {
int x = v[0];
}
64. Constant vector_ints
However, we cannot simply mark operator[] as const, as it will
prevent any further modifica9on to its elements
void p(vector_int const& v) {
int x = v[0];
}
65. Const-overload
To solve this, we provide an addi$onal overload of operator[]
specifically meant for accessing const vectors
class vector_int {
public:
...
int& operator[](std::size_t i) { return elem[i]; }
int operator[](std::size_t i) const { return elem[i]; }
private:
std::size_t sz;
int* elem;
};
66. Const-overload
We can now access elements of constant vector_ints without
preven4ng the possibility of modifying non-const vector_ints
class vector_int {
public:
...
int& operator[](std::size_t i) { return elem[i]; }
int operator[](std::size_t i) const { return elem[i]; }
private:
std::size_t sz;
int* elem;
};
68. Resource acquisi,on
No#ce that in the constructor we allocate memory for the elements
using new
class vector_int {
public:
vector_int(std::size_t sz): sz(sz),
elem(new int[sz]) {}
...
};
69. Memory leak
However, when leaving f(), the heap-allocated elements pointed
to by v.elem are not released
void f() {
vector_int v(10);
int e = v[0];
};
70. Memory leak
We are therefore causing a memory leak
void f() {
vector_int v(10);
int e = v[0];
};
71. Fixing the leak
To solve this, we must make sure that this memory is freed using
delete[]
void f() {
vector_int v(10);
int e = v[0];
// we should call delete[] before
// returning from f()
};
72. The clean_up() member
We could define a clean_up() member func0on
class vector_int {
public:
vector_int(std::size_t sz): sz(sz), elem(new int[sz]) {...}
void clean_up() { delete[] elem; }
...
private:
std::size_t sz;
int* elem;
};
73. The clean_up() member
We can then call clean_up() as follows
void f() {
vector_int v(10);
int e = v[0];
v.clean_up();
};
74. Releasing the memory
Although that would work, one of the most common problems with
the heap is that it is extremely easy to forget to call delete
void f() {
vector_int v(10);
int e = v[0];
v.clean_up();
};
75. Releasing the memory
The equivalent problem would arise for clean_up()
void f() {
vector_int v(10);
int e = v[0];
v.clean_up();
};
77. Destructors
In par'cular, in C++ each class is provided with a special func,on
that makes sure that an object is properly cleaned up before it is
destroyed
79. Destructors
The destructor is a public member func-ons with no input
parameters and no return type
class vector_int {
public:
~vector_int() {...}
...
};
80. Destructors
The destructor has the same name as the class, but with a !lde (~)
in front of it
class vector_int {
public:
~vector_int() {...}
...
};
83. Automa'c memory dealloca'on
Thanks to the presence of the destructor, we do not need to
explicitly release memory
void f() {
vector_int v(10);
int e = v[0];
// Hurray! the dynamic arrays pointed to
// by v.elem is automatically deleted
};
84. Automa'c memory dealloca'on
The tremendous advantage is that a vector cannot forget to call its
destructor to deallocate the memory used for its elements
85. Synthesized destructors
If we do not explicitly provide a destructor, the compiler will
generate a synthesized destructor
86. Synthesized destructors
Such a synthesized destructor invokes the destructors for the
elements (if they have destructors)
87. vector_int synth. destructor
Since sz and elem do not provide a destructor, the synthesized
destructor would simply reduce to an empty func8on
class vector_int {
public:
// synthesized destructor
~vector_int() {}
...
private:
std::size_t sz;
int* elem;
};
88. vector_int synth. destructor
This is why we end up with a memory leak if we do not explicitly
specify a destructor for vector_int
class vector_int {
public:
// synthesized destructor
~vector_int() {}
...
private:
std::size_t sz;
int* elem;
};
90. Copying two vector_ints
Let us try to copy one vector_int into another
void h() {
vector_int v(3);
v[0] = 7;
v[1] = 3;
v[2] = 8;
vector_int w = v; // what happens?
}
91. Copying two vector_ints
Ideally we would like w to become a copy of v
void h() {
vector_int v(3);
v[0] = 7;
v[1] = 3;
v[2] = 8;
vector_int w = v; // what happens?
}
92. Copy seman+cs
That means:
• w.size() == v.size()
• w[i] == v[i] for all i's in [0:v.size())
• &w[i] != &v[i] for all i's in [0:v.size())
94. Copy seman+cs
Furthermore, we want all memory to be released once returning
from h()
void h() {
vector_int v(3);
v[0] = 7;
v[1] = 3;
v[2] = 8;
vector_int w = v;
// here, v.elem and w.elem get released
}
96. Copying is ini*alizing
First, note that w is ini#alized from v
void h() {
vector_int v(3);
v[0] = 7;
v[1] = 3;
v[2] = 8;
vector_int w = v;
}
97. Copying is ini*alizing
We know that ini#aliza#on is done by a constructor
void h() {
vector_int v(3);
v[0] = 7;
v[1] = 3;
v[2] = 8;
vector_int w = v;
}
98. Copying is ini*alizing
Hence, we have to conclude that vector_int provides a
constructor that accepts vector_ints as its only input argument
vector_int(vector_int const& v) {...}
99. Copying is ini*alizing
This is made even more evident if we use an equivalent nota4on
for the last line in h()
void h() {
vector_int v(3);
v[0] = 7;
v[1] = 3;
v[2] = 8;
vector_int w(v); // the same as: vector_int w = v;
}
101. Copy constructor
It turns out that every class in C++ is provided with a special
constructor called copy constructor
vector_int(vector_int const& v) {...}
102. Copy constructor
A copy constructor is defined to take as its argument a constant
reference to the object from which to copy
vector_int(vector_int const& v) {...}
111. Copying pointers
However, memberwise copying in the presence of pointer
members (such as elem) usually causes problems
// synthesized copy constructor
vector_int(vector_int const& v): sz(v.sz),
elem(v.elem) {}
112. Copying pointers
Specifically, w ends up sharing v's elements
void h() {
vector_int v(3);
v[0] = 7;
v[1] = 3;
v[2] = 8;
vector_int w = v;
}
114. Sharing elements
What is the output?
vector_int v(3);
v[0] = 7;
v[1] = 3;
v[2] = 8;
vector_int w = v;
w[0] = 10;
std::cout << v[0];
115. Sharing elements
What is the output?
vector_int v(3);
v[0] = 7;
v[1] = 3;
v[2] = 8;
vector_int w = v;
w[0] = 10;
std::cout << v[0]; // output: 10
116. Double dele)on
Moreover, when we return from h() the destructors for v and w
are implicitly called
void h() {
vector_int v(3);
v[0] = 7;
v[1] = 3;
v[2] = 8;
vector_int w = v;
}
117. Double dele)on
Due to their hidden connec-on, both v and w will try to release the
same memory area
118. A working copy constructor
Hence, we need to explicitly provide a copy constructor that will
1. set the number of elements (i.e., the size)
2. allocate memory for its elements
3. copying the elements from the source vector_int
119. A working copy constructor
class vector_int {
public:
vector_int(vector_int const& v): sz(v.sz),
elem(new int[v.sz]) {
std::copy(v.elem, v.elem + sz, elem);
}
...
private:
std::size_t sz;
int* elem;
};
120. A working copy constructor
void h() {
vector_int v(3);
v[0] = 7;
v[1] = 3;
v[2] = 8;
vector_int w = v;
}
122. No more double dele+on
Given that the two vector_ints are now independent, the two
destructors can do the right thing
void h() {
vector_int v(3);
v[0] = 7;
v[1] = 3;
v[2] = 8;
vector_int w = v;
}
124. Assignment
Even though we now correctly handle copy construc4on,
vector_ints can s4ll be copied by assignment
void g() {
vector_int v(3);
v[0] = 7;
v[1] = 3;
v[2] = 8;
vector_int w(4); w = v;
}
125. Assignment
What happens when we execute the following?
void g() {
vector_int v(3);
v[0] = 7;
v[1] = 3;
v[2] = 8;
vector_int w(4); w = v; // what happens?
}
126. Memory leak and double dele/on
Unfortunately, we once again end up with a memory leak and a
double dele-on
void g() {
vector_int v(3);
v[0] = 7;
v[1] = 3;
v[2] = 8;
vector_int w(4); w = v;
}
127. Synthesized assignment operator
As a ma&er of fact, if we do not provide any overload for the
assignment operator, the compiler will synthesize one for us
131. Synth. assignment for vector_int
Since we did not provide an explicit overload for operator=, the
synthesized assignment is used
vector_int& operator=(vector_int const& v) {...}
132. Working assignment operator
The remedy for the assignment is fundamentally the same as for
the copy constructor
vector_int& operator=(vector_int const& v) {...}
136. Destructors are fundamental
The usage of the constructor / destructor pair for correctly
managing heap-allocated memory is the archetypical example
137. Resource wrappers need destructors
More generally, a class needs a destructor if it acquires resources
138. Resource
A resource is something we get from somewhere and that we must
give back once we have finished using it
• Memory
• File descriptor
• Socket
• ...
139. The big three
A class that needs a destructor almost always needs a copy
construc-on and assignment
140. The big three
The reason is that if an object has acquired a resource the default
meaning of copy is almost certainly wrong
141. The rule of three
This is summarised in the so-called rule of three2
If you need to explicitly declare either the destructor, copy constructor
or assignment operator yourself, you probably need to explicitly declare
all three of them
2
Once again, we are limi/ng ourselves to C++03, in C++11 one would obey either the rule of five or the rule of zero
143. Bibliography
• B. Stroustrup, The C++ Programming Language (4th
ed.)
• B, Stroustrup, Programming: Principles and Prac@ce
Using C++ (2nd
ed.)
• A. Stepanov, Notes on programming
• StackOverflow FAQ, What is the rule of three?