I am going over A Tour of C++ (Section 5.2 Copy and Move). Following the instructions in the book, I have built a container called Vector (it mimics std::vector). My goal is to efficiently implement the following (element-wise) summation:
Vector r = x + y + z;
According to the book, if I don't have move assignment and constructor, + operator will end up copying Vectors unnecessarily. So I implemented move assignment and constructor, but I think the compiler still doesn't use them when I run Vector r = x + y + z;. What am I missing? I appreciate any feedback. Below is my code. I expect to see an output Move assignment, but I get no output. (The summation part works, it's just the move business that I am not sure)
Code
// Vector.h
class Vector{
public:
explicit Vector(int);
Vector(std::initializer_list<double>);
// copy constructor
Vector(const Vector&);
// copy assignment
Vector& operator=(const Vector&);
// move constructor
Vector(Vector&&);
// move assignment
Vector& operator=(Vector&&);
~Vector(){delete[] elem;}
double& operator[](int) const;
int size() const;
void show();
friend std::ostream& operator<< (std::ostream& out, const Vector& vec);
private:
int sz;
double* elem;
};
Vector operator+(const Vector&,const Vector&);
// Vector.cpp
Vector::Vector(std::initializer_list<double> nums) {
sz = nums.size();
elem = new double[sz];
std::initializer_list<double>::iterator it;
int i = 0;
for (it=nums.begin(); it!=nums.end(); ++it){
elem[i] = *it;
++i;
}
}
Vector::Vector(Vector&& vec) {
sz = vec.sz;
vec.sz = 0;
elem = vec.elem;
vec.elem = nullptr;
std::cout<<"Move constructor"<<std::endl;
}
Vector& Vector::operator=(Vector&& vec) {
if (this == &vec){
return *this;
}
sz = vec.sz;
vec.sz = 0;
elem = vec.elem;
vec.elem = nullptr;
std::cout<<"Move assignment"<<std::endl;
return *this;
Vector operator+(const Vector& vec1, const Vector& vec2){
if (vec1.size() != vec2.size()){
throw std::length_error("Input vectors should be of the same size");
}
Vector result(vec1.size());
for (int i=0; i!=vec1.size(); ++i){
result[i] = vec1[i]+vec2[i];
}
return result;
}
}
// Main
int main() {
Vector x{1,1,1,1,1};
Vector y{2,2,2,2,2};
Vector z{3,3,3,3,3};
Vector r = x + y + z;
} // Here I expect the output: Move assignment, but I get no output.
There is a move elision takes place.
According to the C++ 17 Standard (12.8 Copying and moving class objects)
31 When certain criteria are met, an implementation is allowed to omit
the copy/move construction of a class object, even if the constructor
selected for the copy/move operation and/or the destructor for the
object have side effects. In such cases, the implementation treats
the source and target of the omitted copy/move operation as simply two
different ways of referring to the same object, and the destruction of
that object occurs at the later of the times when the two objects
would have been destroyed without the optimization.122 This elision of
copy/move operations, called copy elision, is permitted in the
following circumstances (which may be combined to eliminate multiple
copies):
(31.3) — when a temporary class object that has not been bound to a
reference (12.2) would be copied/moved to a class object with the same
cv-unqualified type, the copy/move operation can be omitted by
constructing the temporary object directly into the target of the
omitted copy/move
So the move constructor is omitted. The second temporary object created by the expression x + y + z; is directly built as the obejct r.
Vector r = x + y + z;
Also take into account that there is move elision in the return statement of the operator
(31.1) — in a return statement in a function with a class return type,
when the expression is the name of a non-volatile automatic object
(other than a function or catch-clause parameter) with the same
cvunqualified type as the function return type, the copy/move
operation can be omitted by constructing the automatic object directly
into the function’s return value
So the temporary object created in the expression x + y (within the operator) will be moved (that is there will be elision relative to the return statement - preceding quote) in the expression ( x + y ) + z where it will be used by constant lvalue reference and the new temporary obejct created in this expression will be built as r.
In whole in this code snippet
Vector x{1,1,1,1,1};
Vector y{2,2,2,2,2};
Vector z{3,3,3,3,3};
Vector r = x + y + z;
due to the move elision there will be created 5 objects of the class Vector.
Related
Today I encountered following UB:
std::vector<double> operator -(const std::vector<double>& this_, const std::vector<double>& other_){
std::vector<double> result;
for(int i = 0; i < this_.size(); i++){
result[i] = this_[i] - other_[i];
}
return result;
}
Basically what I think is when I declare the result the default constructor is called, but it does not initialize the object to a valid state, so when I try to access the element using result[i], it causes undefined behavior.
So my question is this: Does lhs have operation priority over rhs? If not, how can this be UB if it evaluates the rhs first and sets lhs to rhs?
I'm not really familiar with how move works in C++, and I need some help to clarify my understanding. I'm want to overload the operator+, and I'm have a couple of questions about it.
ap_n operator+(const ap_n& n, const ap_n& m) {
ap_n tmp {n};
return tmp += m;
}
My first question is how to make temporary objects movable. As shown in my function above, both arguments are not meant to be mutated, therefore I need to create a third object to do the operation upon.
How can I make my return value usable for move operation. Is the return value supposed to be a reference as ap_n&? Should the return object be encapsulated by std::move(tmp)? Or is it alright as is?
How does C++ decide when an object is rvalue, or decide that a move operation is suitable on an object, and how can I tell the program that an object is safe to use for move operation.
ap_n operator+(const ap_n&, const ap_n&); // defined as in first question
ap_n operator+(ap_n&& n, const ap_n& m) { return n += m; }
ap_n operator+(const ap_n& n, ap_n&& m) { return m += n; }
ap_n operator+(ap_n&& n, ap_n&& m) { return n += m; }
My second question is whether it is necessary to create variants of function that accept rvalue arguments. Right now I have 4 functions, as shown, to be able to accept normal objects and rvalue objects.
Is writing all the combinations possible like this necessary? If I remove all but the first function, would the program still be able to perform move operation correctly?
As a debugging tip, something that can help with getting these things right is to print a message in the move constructor
ap_n(ap_n&& o): x_(std::move(o.x_)) { std::cerr << "Move constructed\n"; }
plus similar messages in other constructors and the destructor. Then you get a clear chronology of when and how instances are created and destroyed.
How can I make my return value usable for move operation. Is the return value supposed to be a reference as ap_n&? Should the return object be encapsulated by std::move(tmp)? Or is it alright as is?
Return the result by value. (Don't return a reference to a local variable, since the local variable goes out of scope immediately, making the reference invalid.) You might find this short article useful: Tip of the Week #77: Temporaries, Moves, and Copies. For more depth, check out cppreference on copy elision.
how to make temporary objects movable
By defining move constructor/assignment to your type.
Is the return value supposed to be a reference as ap_n&?
Not for operator+ when you return new object.
operator += on the other hand returns reference to lhs, so returns ap_n&.
How can I make my return value usable for move operation. Should the return object be encapsulated by std::move(tmp)? Or is it alright as is?
From return statement,
there is an automatic move when returning directly a local variable.
so return tmp; is sufficient.
return std::move(tmp); prevents NRVO
return tmp += m; does a copy, as you don't return "directly" tmp.
You should do:
ap_n operator+(const ap_n& n, const ap_n& m) {
ap_n tmp {n};
tmp += m;
return tmp; // NRVO, or automatic move
}
return std::move(tmp += m); would prevent NRVO, and do the move.
How does C++ decide when an object is rvalue,
Roughly,
variables are l-value as there have name.
function returning l-value reference (ap_n&) returns l-value.
function returning r-value reference (ap_n&&), or by value (ap_n) returns r-value.
or decide that a move operation is suitable on an object, and how can I tell the program that an object is safe to use for move operation.
Overload resolution select the best match between valid candidate.
So it requires function taking by value or by r-value reference (or forwarding reference).
My second question is
It seems not the second ;-)
whether it is necessary to create variants of function that accept rvalue arguments. Right now I have 4 functions, as shown, to be able to accept normal objects and rvalue objects.
Is writing all the combinations possible like this necessary?
Single function taking by const reference or by value can be enough in general case,
unless you want that optimization. so mostly for library writer, or critical code.
Notice that your overloads should be rewritten to effectively do move operation (to reuse input temporary parameter):
ap_n operator+(ap_n&& n, const ap_n& m)
{
n += m;
return std::move(n); // C++11; C++14, C++17
return n; // C++20
}
or
ap_n&& operator+(ap_n&& n, const ap_n& m)
{
return std::move(n += m);
}
If I remove all but the first function, would the program still be able to perform move operation correctly?
With
ap_n operator+(const ap_n& n, const ap_n& m) {
ap_n tmp {n}; // copy
tmp += m;
return tmp; // NRVO, or automatic move
}
You have 1 copy, one NRVO/move for any kind of parameters.
With
ap_n&& operator+(ap_n&& n, const ap_n& m) {
return std::move(n += m);
}
you have no moves, but you should be careful about lifetime of references, as
auto ok = ap_n() + ap_n(); // 1 extra move
auto&& dangling = ap_n() + ap_n(); // No move, but no lifetime extension...
With
ap_n operator+(ap_n&& n, const ap_n& m) {
n += m;
return std::move(n); // C++11, C++14, C++17 // move
return n; // C++20 // automatic move
}
you have 1 move, no copies.
auto ok = ap_n() + ap_n(); // 1 extra move possibly elided pre-C++17, 0 extra moves since C++17
auto&& ok2 = ap_n() + ap_n(); // No moves, and lifetime extension...
So with extra overload, you might trade copy to move.
Taking notes from Jarod42's answer, the following is the revised code.
ap_n operator+(const ap_n& n, const ap_n& m) {
ap_n tmp {n};
tmp += n;
return tmp; // Allows copy, NRVO, or move
}
ap_n operator+(ap_n&& n, const ap_n& m) {
n += m;
return std::move(n); // Allows copy, or move
}
ap_n operator+(const ap_n& n, ap_n&& m) {
m += n;
return std::move(m); // Allows copy, or move
}
The amount of functions is also reduced to 3, since one that takes both rvalue reference will automatically use the second function.
Please tell me if I'm still misunderstanding this.
This is a question I've always pondered on and have never found any resource stating the answer to this question. In fact its not only for +=, but also for its siblings i.e. -=, *=, /=, etc. (of course not ==).
Consider the example,
int a = 5;
a += 4;
//this will make 'a' 9
Now consider the equivalent expression:
a = a + 4;
//This also makes 'a' 9
If += were simply a shorthand for a = a + <rhs of +=>
overloading + operator should also implicitly overload +=, unless explicitly overloaded otherwise. But that isn't what happens. That means, a += b doesn't get converted to a = a + b. But then why wasn't it implemented this way? As in, wouldn't it have been easier to simply convert it to a = a + b during compilation instead of implementing it separately as an operator in itself? That would also help in operator overloading, where a += b, where a and b are objects of the same class would not have to be explicitly overloaded, and simply overloading + would have been enough?
EDIT:
My question becomes more clear with this answer
Let me explain my question with an example where one needs to overload the operators:
class A {
int ivar;
public:
A() = default;
A(int par_ivar) : ivar(par_ivar) { }
A(A& a) {
this.ivar = a.ivar;
}
A(A&& a) noexcept {
this.ivar = a.ivar;
}
A operator+(const A& a) const {
A temp_a;
temp_a.ivar = this.ivar + a.ivar;
return temp_a;
}
void operator=(const A& a) {
this.ivar = a.ivar;
}
~A() = default;
};
Now, let's take a look at the result of 2 programs:
prog1:
int main() {
A a1(2);
A a2(3);
a1 = a1 + a2; //a1.ivar = 5
return 0;
}
prog2:
int main() {
A a1(2);
A a2(3);
a1 += a2; //compilation error!!
return 0;
}
Even when both the programs meant to do, nay, do the same thing, one compiles and runs (hoping that my overloads are correct) the other does not even compile!! Had += been simply replaced by appropriate + and =, we would not have felt the need for an explicit overload of +=. Was this intended to be, or is this a feature waiting to be added?
Operators are not generated from others (except with/from <=> in C++20):
providing operator < doesn't allow a > b (which is indeed "logically" equivalent to b < a). You have to implement all (even by re-using some).
For classes, a += b is not a shorthand for a = a + b
but for a.operator +=(b) or operator +=(a, b)
In the same way a = a + b is a shorthand for a.operator=(operator +(a, b)) (or its variant)
In practice, it is more efficient to implement operator+ from operator += than the reverse.
Even if a user might expect similar behavior according to their names, they are regular functions.
I already saw a matrix iterator for which ++it increase column index whereas it++ increase row index.
If += were simply a shorthand for a = a + <rhs of +=> overloading + operator should also implicitly overload +=, unless explicitly overloaded otherwise. But that isn't what happens. That means, a += b doesn't get converted to a = a + b.
(Possible) rational to not generate might be performance and control:
Vector (for math) or Matrix are good example:
4 possible overloads
Matrix operator+(Matrix&& lhs, Matrix&& rhs) { return std::move(lhs += rhs); }
Matrix operator+(Matrix&& lhs, const Matrix& rhs) { return std::move(lhs += rhs); }
Matrix operator+(const Matrix& lhs, Matrix&& rhs) { return std::move(rhs += lhs); } // + is symmetrical :)
Matrix operator+(const Matrix& lhs, const Matrix& rhs) { auto tmp{lhs}; return tmp += rhs; }
Side effect of the decision allow to give different meanings to operators, as "name operator":
if (42 <in> std::vector{4, 8, 15, 16, 23, 42})
Using a = a + b will imply using a copy assignment (as operator = is used). On the other hand, a += b is by default a compound assignment.
According to cppreference,
copy assignment operator replaces the contents of the object a with a copy of the contents of b (b is not modified).
and
compound assignment operators replace the contents of the object a with the result of a binary operation between the previous value of a and the value of b.
Using a = a + b, would therefore cause an unnecessary memory usage, since a has to be copied once before its value is changed.
can we swap the elements of vector v without using any extra space and return v?
vector<int>func(const vector<int>&v)
{
//write your code here
}
The function returns the vector by value, which means it makes a copy of the input. Hence, you can make the copy explicitly, modify the copy and return it. E.g.:
vector<int> func(vector<int> const& v) {
vector<int> u(v);
// modify u.
return u;
}
One could also make the caller copy the argument vector<int> func(vector<int> u), but that disables return value optimization and hence 2 copies of the vector are made (one for function parameter another for return). See copy elision for full details:
In a return statement, when the operand is the name of a non-volatile object with automatic storage duration, which isn't a function parameter or a catch clause parameter, and which is of the same class type (ignoring cv-qualification) as the function return type. This variant of copy elision is known as NRVO, "named return value optimization".
To modify the vector in-place without copying, the function must take a reference to non-const vector:
vector<int>& func(vector<int>& v) {
// modify v.
return v;
}
While learning about iterators in C++ I tried the following:
#include <vector>
int main() {
std::vector<int> a;
a.end()=a.begin();
//Why is this even allowed by the compiler?
}
What am I missing?
it would not be posiible if for example function end would return a pointer.
For example this code will not be compiled
int a[] = { 1, 2, 3 };
std::end( a ) = std::begin( a );
GCC issues error
error: lvalue required as left operand of assignment
std::end( a ) = std::begin( a );
^
However when objects of class types are used then they can call the ovetloaded copy (or move) assignment operator.
So the question arises why do not the functions return constant iterator objects. I think that it would be possible to apply operator ++. For example
auto it = ++c.begin();
For direct access iterators as pointers you can write simply
auto it = begin( a ) + 1;
The standard doesn't specify whether std::vector::iterator is a class type, or a raw pointer.
If it is a class type then this code calls operator= on the temporary object returned by a.end(). Not a very useful operation, but legal. (Rvalues may have functions called on them).
If your library makes std::vector::iterator be a pointer then this code would fail to compile, since simple assignment requires an lvalue on the left.
Jarod42 pointed out that if the iterator's assignment operator had been defined as:
std::vector::iterator& std::vector::iterator::operator =(std::vector::iterator) &;
then this code would be illegal; the trailing & means that the function is only selectable when it is being called on an lvalue.
But it wasn't defined that way, I would guess that the Committee didn't want to make some legal code illegal for no good reason; maybe there is a use case for it that nobody thought of yet.
The line affect a temporary iterator, and so is useless.
std::vector::iterator = std::vector::iterator is allowed.
A way to disallow that would be to have
std::vector::iterator::operator =(std::vector::iterator) &; // note the extra &
but std::vector::iterator may be a simple pointer, and we can't disalow T* = T*
You cannot assign to an rvalue (aka temporary) primitive type in C++, like (int)7 or a pointer returned by-value from a function. This makes lots of sense, as the result of the assignment would be immediately discarded, so it is probably not what the programmer wanted. rvalue means 'right hand side of the = sign' from language grammars, or a temporary (anonymous) value.
However on objects of class type, = just invokes operator=. Usually when the lhs is an rvalue, this would make no sense, but sometimes it would (say, the class is a proxy object pseudo-reference (like what [] on std::vector<bool> returns). So it is treated nit at all special, it just invokes the method. And there was no way to tell if the method was invoked from a temporary rvalue or a 'real' lvalue.
rvalue references are new in C++11, as are rvalue qualified methods.
Prior to C++11, there was no way to block calling operator= on an object of class type just because it was a temporary.
As begin returns a non-const temporary, if it is of class type (not guaranteed by the standard) it can be assigned to.
Today we can block assignment to temporaries of class type, but the standard library has not been modified to block it on its types. Probably at least partly to avoid backwards compwtibility issues, and partly to determine best practices outside std first.
They are allowed because the iterator objects are copy-assignable and copy-constructible. That is why we can do something like a.begin() = a.end(). Also, we can do something auto it(a.begin()).
Check the examples below regarding copy-assignable.
#include <vector>
struct CopyConsAssignable
{
int x;
CopyConsAssignable(int a)
{
x = a;
}
CopyConsAssignable(const CopyConsAssignable& other)
{
x = other.x;
}
CopyConsAssignable& operator=(const CopyConsAssignable& other)
{
if (this != &other)
{
x = other.x;
}
return *this;
}
};
int main()
{
std::vector<int> a;
a.begin() = a.end();
CopyConsAssignable(2) = CopyConsAssignable(4); // Allowed as the objects are copy-assignable
return 0;
}