Copy, Assignment, and Destructors
reading
Cyganek section 4.4
Copy constructor and assignment operator from Wikipedia
So far, we’ve been relying on the compiler to provide a default copy constructor, assignment operator, and destructor. As long as our data is simple, we can do this, since the compiler will know how to work with it.
Tip
Unless our class is complicated and manages its own memory (e.g., via pointers), we don’t need to write these functions. The compiler will automatically generate them for us.
This is the Rule of 3 (or Rule of 5, as we’ll see later) that we previously mentioned.
Here we’ll explicitly write this out anyway, just to understand how they would look.
We’ll use the Example: Mathematical Vectors Vector2d
class as our example here.
Copy constructor
The copy constructor is invoked when we do something like:
Vector2d vec(1, 2);
auto vec2(vec);
auto vec3 = vec;
In both of these cases, we need to create a new Vector2d
with
its own memory and copy the data over. The copy constructor has
the function signature:
Vector2d(const Vector2d& vec)
Assignment operator
The assignment operator is invoked when we do something like:
Vector2d vec(1, 2);
Vector2d vec2;
vec2 = vec;
The assignment operator has the signature:
Vector2d& operator= (const Vector2d& vec)
Note
The reason that the assignment operator returns a reference is to allow us to chain assignment, e.g.:
v1 = v2 = v3;
Destructor
The destructor is written as:
~Vector2d()
Its job is to clean up any of the memory we allocated. But for our
Vector2d
class, all the data are automatic variables, so they will
be deleted when they go out of scope by the stack. This is the reason
we really don’t need to write a destructor for this class.
Example
Here’s an example of our Vector2d
class that explicitly implements
the copy constructor, assignment operator, and destructor.
Additionally, we add a print to stdout in each of these so we can see
when each of them are called.
#ifndef VECTOR_2D_H
#define VECTOR_2D_H
#include <iostream>
#include <cmath>
class Vector2d {
private:
// our member data
double x;
double y;
public:
// default constructor
Vector2d()
: x{0.0}, y{0.0}
{}
// another constructor
Vector2d(double _x, double _y)
: x{_x}, y{_y}
{}
// the destructor
~Vector2d() {std::cout << "in the destructor" << std::endl;}
// copy and assignment constructors -- we don't need to define
// them, but we explicitly say to use the default
//Vector2d(const Vector2d& vec) = default;
Vector2d(const Vector2d& vec)
: x(vec.x), y(vec.y)
{std::cout << "in the copy constructor" << std::endl;}
// assignment here means that we want to set our current class's
// values to those to the right of the = -- that's the Vector2d
// coming in through the argument list
//Vector2d& operator= (const Vector2d& vec) = default;
Vector2d& operator= (const Vector2d& vec) {
std::cout << "in the assignment operator" << std::endl;
x = vec.x;
y = vec.y;
return *this;
}
// setters
inline void set_x(double _x) {x = _x;}
inline void set_y(double _y) {y = _y;}
// operators
// add two vectors
Vector2d operator+ (const Vector2d& vec) {
Vector2d vec_out(x + vec.x, y + vec.y);
return vec_out;
}
// subtract two vectors
Vector2d operator- (const Vector2d& vec) {
Vector2d vec_out(x - vec.x, y - vec.y);
return vec_out;
}
// unary minus
Vector2d operator- () {
Vector2d vec_out(-x, -y);
return vec_out;
}
// multiplication
// vec * a
Vector2d operator* (double a) {
Vector2d vec_out(a * x, a * y);
return vec_out;
}
// a * vec -- needs to be a friend
friend Vector2d operator* (double a, const Vector2d& vec);
// division by a scalar
Vector2d operator/ (double a) {
Vector2d vec_out(x / a, y / a);
return vec_out;
}
// comparison operators
bool operator== (const Vector2d& vec) const {
return x == vec.x && y == vec.y;
}
bool operator!= (const Vector2d& vec) const {
return x != vec.x || y != vec.y;
}
double abs() {
return std::sqrt(x * x + y * y);
}
double dot(const Vector2d& vec) {
return x * vec.x + y * vec.y;
}
// << and >> are not class members, but need access to the member data
friend std::ostream& operator<< (std::ostream& os, const Vector2d& v);
friend std::istream& operator>> (std::istream& is, Vector2d& v);
};
inline
std::ostream& operator<< (std::ostream& os, const Vector2d& vec)
{
os << "(" << vec.x << ", " << vec.y << ")";
return os;
}
inline
std::istream& operator>> (std::istream& is, Vector2d& vec)
{
is >> vec.x >> vec.y;
return is;
}
inline
Vector2d operator*(double a, const Vector2d& vec) {
Vector2d vec_out(a * vec.x, a * vec.y);
return vec_out;
}
#endif
Here’s a driver that exercises them.
#include <iostream>
#include "vector2d.H"
int main() {
Vector2d v1(1.0, 2.0);
Vector2d v2, v3;
// test assignment
std::cout << "doing v3 = v2 = v1" << std::endl;
v3 = v2 = v1;
std::cout << v2 << " " << v3 << std::endl;
// test copy
std::cout << "doing auto v4 = v1" << std::endl;
auto v4 = v1;
std::cout << "doing auto v5(v1)" << std::endl;
auto v5(v1);
std::cout << "doing auto v6{v1}" << std::endl;
auto v6{v1};
std::cout << v4 << std::endl;
}
Note
We see that the destructor is called for each of the Vector2d
‘s we created
when the program ends.
Tip
If we want to explicitly provide one of these functions but just do what the
compiler would do, we can use default
, e.g.:
Vector2d(const Vector2d& vec) = default;
Tip
If we want to forbid an operation, then we can use delete
, e.g.:
Vector2d(const Vector2d& vec) = delete;