Help with copy/swap semantics for class with smart pointer

Pages: 12
Hello. I recently started learning about smart pointers and such, mutexes and what-not. I wanted to make an int array class that holds a dynamically allocated array and has a destructor that will call delete on the array when called. I decided to use smart pointers instead, but I don't know how to design the move semantics or copy semantics. In my last question I asked how to do this with a raw pointer. Sorry, I don't have any code to build off of this time.
Last edited on
Something like this, perhaps:
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
#include <iostream>
#include <memory>
#include <algorithm>

template <typename T>
class Array {
    size_t m_size;
    std::unique_ptr<T[]> m_data;
public:
    Array(size_t size) : m_size(size), m_data(new T[m_size]) { }

    Array(const Array& o) : m_size(o.m_size), m_data(new T[m_size]) {
        std::copy(&o.m_data[0], &o.m_data[o.m_size], &m_data[0]);
    }

    Array(Array&& o) : m_size(o.m_size), m_data(std::move(o.m_data)) {
        o.m_size = 0;
    }

    T& operator=(T other) noexcept {
        std::swap(m_size, other.m_size);
        std::swap(m_data, other.m_data);
        return *this;
    }
};

int main() {
    Array<int> a(100);
    Array<int> b(a);
    Array<int> c(std::move(b));
}
Last edited on
Alright. Thanks! Let me clear up one thing:

So, when an Array object is passed to the assignment operator, it first goes to the copy constructor (correct?), which copies the individual elements in o.m_data to this->m_data and then it goes to the assignment operator, which swaps its copied data with this and then destroys the copy? This seems to make sense, but I'm not sure.

Also, isn't line 10 evil? Since it calls T's constructor with new, rather than std::make_unique? After all, T's constructor may throw an exception.
Good point about make_unique.

The assignment operator works for both copy and move assignment by using the copy and move constructors on the parameter other. The argument will either be copied or moved to other; then it's contents are swapped with this; then the destructor will be called on other.

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
#include <iostream>
#include <memory>
#include <algorithm>

template <typename T>
class Array {
    size_t        m_size;
    std::unique_ptr<T[]> m_data;
public:
    Array(size_t size) : m_size(size), m_data(std::make_unique<T[]>(m_size))
    {
    }

    Array(const Array& o) : Array(o.m_size)
    {
        std::copy(&o.m_data[0], &o.m_data[o.m_size], &m_data[0]);
    }

    Array(Array&& o) : m_size(o.m_size), m_data(std::move(o.m_data))
    {
        o.m_size = 0;
    }

    T& operator=(T other) noexcept
    {
        std::swap(m_size, other.m_size);
        std::swap(m_data, other.m_data);
        return *this;
    }
};

int main() {
    Array<int> a(100);
    Array<int> b(a);
    Array<int> c(std::move(b));
}

Alright, thanks for clearing that up. Also, isn't &o.m_data[0] the same as just typing o.m_data?
Last edited on
That would work for a raw pointer, but not for the std::unique_ptr class.
Oh, right. Thanks, I noticed though, operator= assumes that T has member values m_data and m_size. Did you mean Array<T>& operator=(Array<T> other) noexcept?

EDIT: It also says that *this is of type T, which it can't be.
Last edited on
I'm glad one of us is paying attention! You're right, of course.

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
#include <iostream>
#include <memory>
#include <algorithm>

template <typename T>
class Array {
    size_t m_size;
    std::unique_ptr<T[]> m_data;
public:
    Array(size_t size) : m_size(size), m_data(std::make_unique<T[]>(m_size))
    {
    }

    Array(const Array& o) : Array(o.m_size)
    {
        std::copy(&o.m_data[0], &o.m_data[o.m_size], &m_data[0]);
    }

    Array(Array&& o) : m_size(o.m_size), m_data(std::move(o.m_data))
    {
        o.m_size = 0;
    }

    Array<T>& operator=(Array<T> other) noexcept
    {
        std::swap(m_size, other.m_size);
        std::swap(m_data, other.m_data);
        return *this;
    }
};

int main() {
    Array<int> a(100);
    Array<int> b(a);
    Array<int> c(std::move(b));
    a = b;
    b = std::move(c);
}

Hahaha, well I'm glad one of us had some base code at least. Thank you!

EDIT: The finished product seems to work splendidly! :D
Last edited on
You should also have move assignment as well as copy assignment. This will mean a change to copy assignment as you can't have function overloads with a param passed by value and by refref.
> You should also have move assignment as well as copy assignment.

There is nothing wrong in using the copy and swap idiom to implement a unifying assignment operator. Though, sometimes it may not be as efficient as separate, carefully crafted, copy and move assignment operators.
https://www.cs.helsinki.fi/group/boi2016/doc/cppreference/reference/en.cppreference.com/w/cpp/language/as_operator.html#Copy_and_swap
But unless I'm still recovering from Wednesday (England win! - possible..), surely for L24 other is passed by value which involves a copy in all cases.....

Why does the OP need a move ctor? Doesn't unique_ptr implement it ?
Yep - I'm still befuddled from Wednesday's celebrations. You can't copy a std::unique_ptr, only move it.

Ahhhh. Oh well. At least I enjoyed Wednesday night - I think...

If I'm like this now after Wednesday's win - I'll probably be a gibbering idiot next week if England win on Sunday.........
Why does the OP need a move ctor? Doesn't unique_ptr implement it ?


Lets try this with a wet towel around my head to see if I can talk sense....

You can't pass by value in a constructor as you're then got infinite recursion. Some compilers report this as an error. So:

 
Array(Array<T> o) {...}


won't compile like it does for assignment. If you just have:
 
Array(Array&& o) {...}


then code such as:

 
Array<int> b(a);


wont compile as there's no matching constructor.

So you have to have:

1
2
Array(const Array& o) {...}
Array(Array&& o)  {...}




Last edited on
> Why does the OP need a move ctor? Doesn't unique_ptr implement it ?

It is not really needed; the class itself is not really needed when we have std::vector
I think SirEnder125's intention was to learn about implementing the foundation operations in a class.


> You can't pass by value in a constructor as you're then got infinite recursion.

This was your original assertion, on which I commented:
'You should also have move assignment as well as copy assignment.'
That was about the assignment operator, not the constructor.

I pointed out that 'There is nothing wrong in using the copy and swap idiom to implement a unifying assignment operator.' Assignment operator, not constructor.


Lets try this again...

L24. other is passed by value. So other is created via a call to the copy constructor. The copy constructor can throw, so operator=() shouldn't be marked noexcept.

Whether the passed arg is an l-value or a r-value, a copy is still undertaken. So move semantics are not being implemented for assignment.

In order to have move semantics for assignment, we need the two versions for l-value and r-value.

Possibly (excluding making swap swappable :) ):

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
#include <iostream>
#include <memory>
#include <algorithm>

template <typename T>
class Array {
	size_t m_size {};
	std::unique_ptr<T[]> m_data;

public:
	Array(size_t size) : m_size(size), m_data(std::make_unique<T[]>(size)) { }

	Array(const Array& o) : Array(o.m_size) {
		std::copy_n(o.m_data.get(), o.m_size, m_data.get());
	}

	Array(Array&& o) noexcept : m_size(o.m_size), m_data(std::move(o.m_data)) {
		o.m_size = 0;
	}

	void swap(Array& other) noexcept {
		std::swap(m_size, other.m_size);
		std::swap(m_data, other.m_data);
	}

	Array& operator=(Array&& other) noexcept {
		swap(other);
		return *this;
	}

	Array& operator=(const Array& other) {
		auto tmp {other};

		swap(tmp);
		return *this;
	}
};

int main() {
	Array<int> a(100);
	Array<int> b(a);
	Array<int> c(std::move(b));
	//a = b; b is now a moved-from object so don't use
	b = std::move(c);
	a = b;
}

> Whether the passed arg is an l-value or a r-value, a copy is still undertaken.
> So move semantics are not being implemented for assignment.

For lvalue arguments, the copy constructor is invoked (a copy is made).
For rvalue arguments, the move constructor is invoked (the object is moved).
Move semantics are used for rvalues, and copy is used for lvalues
That is why it is referred to as a unifying assignment operator.

I had posted a link in my very first post in this thread; consider reading it.
Last edited on
OK. I haven't done it that way before - I've always used 2 functions. Never too late to learn! But in this case of the unifying assignment, shouldn't it not be marked noexcept as in the case of a lvalue, the copy constructor called can throw? The link doesn't show as noexcept.

Thanks for the info. :) :)
Last edited on
> shouldn't it not be marked noexcept as in the case of a lvalue, the copy constructor called can throw?

With a non-throwing swap, I would mark it as noexcept

noexcept is primarily used to indicate to the compiler that the function would not throw exceptions; which is the case for the unifying assignment operator with a non-throwing swap. The copy constructor may throw; but that throw would take place at the call site, in the context of the caller, before the unifying assignment operator is entered into.

IS:
When a function is called, each parameter is initialized with its corresponding argument. ...
The initialization and destruction of each parameter occurs within the context of the calling function.
http://eel.is/c++draft/expr.call#7
Pages: 12