Reputation: 11
I'm trying to understand how copy and move semantics work in a linear algebra vector class I'm writing with an overloaded operator+. I've intentionally deleted the move assignment operator to see what the compiler (Clang, C++17) will do when I add two Vector objects. Unfortunately, I'm having trouble understanding why the copy assignment operator is never called:
class Vector
{
public:
explicit Vector(std::vector<int> v) : data(v){};
// Default constructor.
Vector() = default;
// Copy operations.
Vector(const Vector&);
Vector& operator=(const Vector&);
// Move operations.
Vector(Vector&&) = delete;
Vector& operator=(Vector&&) = delete;
private:
std::vector<int> data;
std::size_t rows = 0;
std::size_t cols = 0;
};
// Copy constructor.
Vector::Vector(const Vector& v1)
{
data = v1.data;
rows = v1.rows;
cols = v1.cols;
std::cout << "Copy construction invoked." << std::endl;
}
// Copy assignment.
Vector& Vector::operator=(const Vector& v1)
{
data = v1.data;
rows = v1.rows;
cols = v1.cols;
std::cout << "Copy assignment invoked." << std::endl;
return *this;
}
// Operator+
Vector operator+(const Vector& v1, const Vector& v2)
{
// Send back a dummy vector.
Vector v3{{1,2,13}};
return v3;
}
The main.cpp file is:
int main()
{
std::vector<int> q{1,2,31};
// Explicitly construct w and copy construct r.
Vector w(q);
Vector r(w);
// Should be copy assignment if move operations are deleted?
Vector u = w + r;
return 0;
}
I would have expected if the move assignment operator is deleted then the copy assignment operator would have been used to fill in the Vector u. However I don't see "Copy assignment invoked" printed to std::cout at all. What am I missing?
Upvotes: 1
Views: 689
Reputation: 275310
There is two things going on here. First, =
doesn't always mean assignment; it sometimes means construction. Second, you are running into a feature of C++ called elision.
=
doesn't always mean operator=
Vector u = w + r;
while this has the =
character, you are constructing a brand-new Vector
object. The use of =
doesn't always mean you are assigning; for assigning to occur, the object you are assigning-to has to exist before you use =
to put a value into it.
Now in your code you attempt to track both the copy constructor and the copy assignment operator, and neither prints. Which leads to the second thing that is confusing you: elision.
Elision is an operation that is permitted, and sometimes demanded, by the C++ standard.
When elision occurs, two objects lifetime is merged into one.
When you do:
Vector v1, v2;
Vector v_result = v1+v2;
we get 2 instances of elision. The first is within operator+
.
// Operator+
Vector operator+(const Vector& v1, const Vector& v2)
{
// Send back a dummy vector.
Vector v3{{1,2,13}};
return v3;
}
the local variable Vector
and the return value of operator+
are elided together. This elision is not guaranteed, but is permitted. When the elision occurs, no copy constructor runs because the two objects lifetime and identity are merged.
The second instance of elision is here
Vector v_result = v1+v2;
the lifetime of the return value of operator+
and the local variable v_result
are elided. Under c++17 this is mandatory. No copy occurs, because there is only one object.
Elision is transitive, so the v_result
object is the same object as the v3
within operator+
and is the same object as the return value of operator+
. The 3 have been elided together.
Now, when elision is not guaranteed by the C++ standard, a copy or move constructor has to exist. But when it is guaranteed, no such constructor need exist.
Assuming your compiler is C++17 or greater, you could do:
Vector operator+(Vector const&, Vector const&) {
return Vector{{1,2,3}};
}
and even if you deleted the copy constructor the code would compile and run.
Elision even occurs over DLL boundaries. (This basically requires that the ABI of C++ provide a pointer-to-return-object as part of the calling convention; as all major ABIs did so when C++17 was rolled out, this was acceptable)
The side effect of this is that the meaning of copy/move construction is restricted by the C++ standard, as it must support elision as being a sensible replacement.
If you really want to see the copy constructor being called:
// Operator+
Vector operator+(const Vector& v1, const Vector& v2)
{
// Send back a dummy vector.
Vector v3{{1,2,13}};
Vector v4{{4,5,6}};
if (rand())
return v3;
else
return v4;
}
here I make 2 different objects with overlapping lifetime. This will block many compilers from eliding their existence with the return value.
Upvotes: 4