Trying to understand move semantics. Please comment

Sep 4, 2016 at 10:06pm
I wrote the following test implementation trying to wrap my head around the concept of move semantics and constructors and assignments in general. I was wondering if I got it right.

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#include <iostream>
#include <utility>

class member{
	public:
	member() {
		std::cout << "Member default constructor" << std::endl;
		mymember=new int(5);
	}
	
	member(int x) {
		std::cout << "Member default constructor with param" << std::endl;
		mymember=new int(x);
	}
	
	~member() {
		delete mymember;
	}
	
	member(member const &rhs)  {  //copy constructor uses a const lvalue reference  to create a new object
		std::cout << "Member copy constructor due to parameter of &rhs" << std::endl;
		mymember=new int(*(rhs.mymember));
	}
	
	member(member &&rhs){ //move constructor  uses an rvalue reference as parameter to create a new object
		std::cout << "Member move constructor due to parameter of &&rhs" << std::endl;
		mymember=new int(*(rhs.mymember));
		std::swap(mymember,rhs.mymember); //and does a swap
		rhs.mymember=nullptr;
	}
	
	bool operator==(member const &rhs){
		return (mymember==rhs.mymember);
	}
	
	//CANNOT overload member & operator=(member rhs) 
	/*member & operator=(member &rhs ){
		std::cout << "Member assignment operator" << std::endl;
		if (!(this == &rhs)) {
			member tmp(rhs);
			mymember=tmp.mymember; //assign value of tmp's pointer to mymember
			tmp.mymember=nullptr; //tmp is destructed but *this inherited the pointer and the resource it is pointing to
			                       //rhs is untouched and preserves its validity  
			                       
		}
		return *this;
	}*/
	
	//ERROR, intent to destroy parameter of std::move(x) not carried out due to temporary was used
	/*member & operator=(member &&  rhs){ 
		std::cout << "Member move assignment operator" << std::endl;
		if (!(this == &rhs)) {
			member tmp(rhs);  //creates a temporary copy from rhs
			std::swap(mymember,tmp.mymember);
			tmp.mymember=nullptr;  //tmp is destructed, but rhs is untouched and preserves validity
		}
		return *this;
	}*/
	
	//MOVE assignment operators should not have a temporary like copy assignment
	//CANNOT overload member & operator=(member rhs),
	/*member & operator=(member &&  rhs){ 
		std::cout << "Member move assignment operator" << std::endl;
		if (!(this == &rhs)) {
			std::swap(mymember,rhs.mymember);
			rhs.mymember=nullptr;  //rhs is destroyed
		}
		return *this;
	}*/
	
	
	//move assignment with COPY AND SWAP IDIOM cannot overload the operator with &rhs and &&rhs parameters
	member & operator=(member rhs){ // a temporary is automatically created depending on wether 
		                            // an rvalue reference is provided -> move constructor to create value copy
		                            // an object is provided -> copy constructor to create value copy
		std::cout << "Member move assignment operator" << std::endl;
		if (!(this == &rhs)) {
			std::swap(mymember,rhs.mymember);  
		}
		return *this;
	}
	
	int *mymember;
};



#include <iostream>

int main(int argc, char **argv)
{
		
	member a;  std::cout << "a " << (*a.mymember) << std::endl;
	member b(6);  std::cout << "b " << (*b.mymember) << std::endl;
	
	std::cout << "------------------------------------" << std::endl;
	std::cout <<"member c=a" << std::endl;
	//copy constructor, create c by copying a
	member c=a; std::cout << "c should be 5 ==> " << (*c.mymember) << std::endl;
	
	std::cout << "------------------------------------" << std::endl;
	//move constructor, create d and swap d and an rvalue reference from a
	//the intent is to never again use a
	std::cout << "member d=std::move(a)" << std::endl;
	member d=std::move(a);   std::cout << "d should be 5 ==>" << (*d.mymember) << std::endl;
	                         std::cout << "a should be ?? ==>" << "SEG FAULT" << std::endl; 
	
	std::cout << "------------------------------------" << std::endl;
	//assignment operator, copy of b created into tmp object and tmp is assigned to c, b remains valid
	std::cout << "c=b" << std::endl;
	c=b; std::cout << "c should be 6 ==> " << (*c.mymember) << std::endl;
	     std::cout << "b should be 6 ==> " << (*b.mymember) << std::endl;
	
	std::cout << "------------------------------------" << std::endl;
	//move assignment operator, existing d object is assigned via rvalue reference of c 
	//the intent is to never again use c
	std::cout << "d=std::move(c)" << std::endl;
	d=std::move(c); std::cout << "d should be 6 ==> " << (*d.mymember) << std::endl;
	                std::cout << "c should be ?? ==> " << "SEG FAULT" << std::endl; 
	
	std::cout << "------------------------------------" << std::endl;
	
	return 0;
}


Are these points correct?
1. Copy and swap idiom's objective is to make the class's move operations exception safe and since the potential failure occurs with memory allocation,
this only applies to classes with pointer members.

2. Shouldn't use a temporary for move assignment operators because the intent is to destroy (never use) the parameter of std::move

3. Cannot overload this combination of assignment operators : (case A and B are fine until case C is defined)
case A: member & operator=(member &&rhs) parameter from std::move
case B: member & operator=(member &rhs ) parameter is object
case C: member & operator=(member rhs) value is passed, this apparently is an ambiguous overload to cases A and B.

*** However, the last case (C) covers the first 2 cases, differenting only in terms of which (copy or move constructor ) to call to create the temporary value based on wether a &&rhs (via std::move) is provided or a &rhs by passing an object. The intent to destroy the parameter of std::move is preserved as well.
4. Did I use std::swap correctly. It seems to work. The concept of creating an std::swap overload for the class objects above seems unecessary here as I am swapping a built in type (pointer) inside the class. But I think I could just as well write an overload that deals with the class above directly by simply calling std::swap in the body with a pointer parameter.

Thanks
Chris
Sep 5, 2016 at 8:22am
1. No. It has nothing to do with exceptions

2. Temporaries are perfectly fine for use with move (actually it was one of the reasons to introduce this).

3. In case of line 73: rhs is always a copy and will never be this == &rhs. swap() is okay since rhs will be destroyed at the end of the function. You shouldn't implement such an operator.

4. Swap doesn't make sense within the move semantic. It is expected that the passed object is empty afterwards (whatever is considered as 'empty')

[Edit]See:

http://en.cppreference.com/w/cpp/language/rule_of_three

What to implement in case of copy/move.
Last edited on Sep 5, 2016 at 8:25am
Sep 5, 2016 at 9:34am
1. Copy and swap idiom's objective is to make the class's move operations exception safe

To make the (unifying) assignment operator exception-safe.


2. Shouldn't use a temporary for move assignment operators because the intent is to destroy (never use) the parameter of std::move

Using a temporary to move from is fine.
The intent is to never use an object after it has been moved from. Unless there are special guarantees, as with types defined by the standard library.


3. Cannot overload this combination of assignment operators :

Yes. Either have separate overloaded copy assignment and move assignment operators or have a single unifying assignment operator.


4. Did I use std::swap correctly. It seems to work.

Yes; though writing a custom swap (swap pointer, swap size) would be more efficient than (unspecialised) std::swap (move construct, move assign, move assign, destroy trivially).


The class with a custom non-trowing swap, and a unifying assignment operator:
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
struct my_class
{
    my_class() { std::cout << "default constructor\n" ; }
    explicit my_class( std::size_t n ) : ptr( new int[n]{} ), sz(n) { std::cout << "constructor with arg\n" ; }

    my_class( const my_class& that ) : ptr( new int[that.sz]{} ), sz(that.sz)
    {
        std::copy( that.ptr, that.ptr+sz, ptr ) ;
        std::cout << "copy constructor\n" ;
    }

    my_class( my_class&& that ) noexcept : ptr(nullptr), sz(0)
    {
        this->swap(that) ;
        std::cout << "move constructor\n" ;
    }

    // unifying assignment operator: both move assignment and exception-safe copy assignment
    // see: 'Copy-and-swap idiom' http://en.cppreference.com/w/cpp/language/copy_assignment
    // see: https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Copy-and-swap
    my_class& operator= ( my_class that ) noexcept
    {
        this->swap(that) ;
        std::cout << "unifying assignment operator: copy(lvalue)/move(rvalue) and swap\n" ;
        return *this ;
    }

    ~my_class() noexcept { delete[] ptr ; std::cout << "destructor\n" ; }

    void swap( my_class& that ) noexcept { using std::swap ; swap(ptr,that.ptr) ; swap(sz,that.sz) ; }

    private:
        int* ptr = new int[5] {} ;
        std::size_t sz = 5 ;
};

http://coliru.stacked-crooked.com/a/4b7c7d3ee75e8727
Last edited on Sep 5, 2016 at 9:43am
Sep 5, 2016 at 1:25pm
Thanks for your replies. The picture is clearer to me now.

Things that I learned:

I forgot to delete the
 
if (!(this == &rhs))  

in line 77.

1. The address of a copy will never be equal to the address of the original. My overload of the == operator is not even needed as I would be doing a pointer to pointer comparison (not object to object) if I decided to use both :

1
2
member & operator=(member &&rhs) 
member & operator=(member &rhs )


instead of the universal unifying assignment operator:

 
member & operator=(member rhs )


Although I am wondering now what address rhs has when the parameter is &&rhs.

2. std::swap is exception safe and could be used to build exception safe user defined overloads that swap built in types in the function body.

3. Using a swap makes sense in move semantics if the temporary's values are zeroed out (pointers set to null) to begin with such that the swap results in the object being moved from aquiring default (nullptr for pointers) values. The change is propagated to the original object (as if we passed a &rhs instead of a &&rhs).

Question : So a &&rhs behaves like an &rhs in the function/constructor body?
Question : When is the object moved from deleted? Are there no automatic measures to delete the moved from object except when it goes out of scope?

4. The assignment operator is "unifying" in the sense that it invokes the copy or move constructor to create the copy.

Question: Isn't the copy constructor not exception safe due to the call to new? So that if it is invoked by the assignment operator, the assignment operator wouldn't be exception safe as well?
Question: If the copy constructor is not exception safe (depends on answer to first question), would it be better to disable the copy constructor and just invoke move constructor all the time? How would this be done?
Question: Does the compiler check to see if the function cannot throw an exception when you use noexcept?


Thanks
Chris




Sep 5, 2016 at 4:47pm
> 1. I am wondering now what address rhs has when the parameter is &&rhs

A reference is an alias for an object. Since a references is not an object, it may not occupy any storage; it does not have an address. Declaring a pointer to a reference (eg. int&* or int&&*) is an error. The addressof operator applied to a reference would yield the address of the object for which it is an alias.


> 2. std::swap is exception safe

The std::swap provided by the library is exception-safe (noexcept) if the move constructor and the move assignment operator of the type involved is non-throwing.


> 2. std::swap could be used to build exception safe user defined overloads that swap built in types

Yes.


> 3 So a &&rhs behaves like an &rhs in the function/constructor body?

Anything that has a name is an lvalue. Within the function
void foo( int&& arg ) { /* ... */ }, arg is an lvalue.


> 3 When is the object moved from deleted? Are there no automatic measures to delete the moved from object

Whether an object was moved from or not does not affect its lifetime in any specific way.
The usual rules for object-lifetime would be applicable for moved from objects too.


> 4. Isn't the copy constructor not exception safe due to the call to new? So that if it is invoked
> by the assignment operator, the assignment operator wouldn't be exception safe as well?

Exception-safe assignment does not imply that invoking the assignment operator won't throw. It means that if an exception is thrown, the objects involved will retain their original state; that assignment is an all or nothing operation. (If the copy constructor throws, the assignment operator is not invoked.)


> 4. Does the compiler check to see if the function cannot throw an exception when you use noexcept?

No, it does not. noexcept is a promise made by the programmer; the compiler can use this information to perform optimisations that wouldn't be possible if exceptions could be thrown.
Sep 5, 2016 at 10:09pm
Thanks a lot. This puts a lot of concepts in proper contexts for me. I will continue my studies.
Thanks for your time and interest
Chris
Topic archived. No new replies allowed.