The best way to achieve what you want is to use a class that already handles assignment, copying and moving, taking care of its memory management for you. std::vector
does exactly this, and can directly replace your dynamically allocated array and size. Classes that do this are often referred to as RAII classes.
Having said that, and assuming this is an exercise in correctly implementing the various special member functions, I'd suggest that you proceed via the copy and swap idiom. (See What is the copy and swap idiom? on SO, for more details and commentary). The idea is to define the assignment operation in terms of the copy constructor.
Start with the members, constructor and destructor. These define the ownership semantics of the members of your class:
template <class data>
class foo {
public:
foo(const size_t n);
~foo();
private:
size_t size; // array size
size_t cursor; // current index
data* dataArray; // dynamically allocated array
};
template <class data>
foo<data>::foo(const size_t n)
: size(n), cursor(0), dataArray(new data[n])
{}
template <class data>
foo<data>::~foo() {
delete[] dataArray;
}
Here, memory is allocated in the constructor and deallocated in the destructor.
Next, write the copy constructor.
template <class data>
foo<data>::foo(const foo<data>& other)
: size(other.size), cursor(other.cursor), dataArray(new data[other.size]) {
std::copy(other.dataArray, other.dataArray + size, dataArray);
}
(along with the declaration, foo(const foo& other);
inside the class body).
Notice how this uses member initialiser lists to set the member variables to the values in the other
object. A new allocation is performed, and then in the body of the copy constructor you copy the data from the other
object into this object.
Next comes the assignment operator. Your existing implementation has to perform a lot of manipulation of pointers, and isn't exception safe. Let's look at how this could be done more simply and more safely:
template <class data>
foo<data>& foo<data>::operator=(const foo<data>& rhs) {
foo tmp(rhs); // Invoke copy constructor to create temporary foo
// Swap our contents with the contents of the temporary foo:
using std::swap;
swap(size, tmp.size);
swap(cursor, tmp.cursor);
swap(dataArray, tmp.dataArray);
return *this;
}
(along with the declaration in-class, foo& operator=(const foo& rhs);
).
[-- Aside: You can avoid writing the first line (explicitly copying the object) by accepting the function argument by value. It's the same thing, and might be more efficient in some cases:
template <class data>
foo<data>& foo<data>::operator=(foo<data> rhs) // Note pass by value!
{
// Swap our contents with the contents of the temporary foo:
using std::swap;
swap(size, rhs.size);
swap(cursor, rhs.cursor);
swap(dataArray, rhs.dataArray);
return *this;
}
However, doing so may cause ambiguous overloads if you also define a move assignment operator. --]
The first thing this does is create a copy of the object being assigned from. This makes use of the copy constructor, so the details of how an object is copied need only be implemented once, in the copy constructor.
Once the copy has been made, we swap our internals with the internals of the copy. At the end of the function body, the tmp
copy goes out of scope, and its destructor cleans up the memory. But this isn't the memory that was allocated at the beginning of the function; it's the memory our object used to hold, before we swapped our state with the temporary.
In this way, the details of allocating, copying and deallocating are kept where they belong, in the constructors and the destructor. The assignment operator simply copies and swaps.
This has a further advantage, over and above being simpler: It's exception safe. In the code above, an allocation error could cause an exception to be thrown while creating the temporary. But we haven't modified the state of our class yet, so our state remains consistent (and correct) even when the assignment fails.
Following the same logic, the move operations become trivial. The move constructor must be defined to simply take ownership of the resource and leave the source (the moved-from object) in a well-defined state. That means setting the source's dataArray
member to nullptr
so that a subsequent delete[]
in its destructor doesn't cause problems.
The move assignment operator can be implemented similarly to the copy assignment, although in this case there's less concern with exception safety since you're just stealing the already-allocated memory of the source object. In the complete example code, I opted to simply swap the state.
A complete, compilable-and-runnable example can be seen here.