Category:2012-08-30 21:40 2017 people reading reviews (2) favorite reports
Any analogy that manages a resource, such as a smart pointer, needs to follow a rule (the rule of three):
If you need to explicitly declare one of the three: destructors, copy constructors, or copy assignment operators, you need to explicitly declare all three of them.
Copy constructors and destructors are easier to implement, but copy assignment operators are much more complex.
How is it implemented? What mistakes do we need to avoid?
Then Copy-and-swap is the perfect solution. It is also good to help copy assignment operators reach two goals: avoid code duplication, and provide strong exception security assurances.
1. How to Work
Conceptually, it uses the copy constructor to generate a temporary copy, and then uses the SWAP function to exchange the copied object with the old data. The temporary object is then refactored, and the old data disappears. We have a copy of the new data.
To use Copy-and-swap, we need to copy constructors, destructors, and swap functions.
An interchange function is a non-throwing function that swaps two objects of a class and swaps them by member. We might try to use Std:swap, but that's not possible. Because Std:swap uses its own copy constructor and copy assignment operator. And our goal is to define our own copy assignment operators.
2. Purpose
Let's look at a concrete example. We need to manage a dynamic array in a class. We need to implement constructors, copy assignment operators, and destructors.
[CPP]View Plaincopyprint?
- #include <algorithm>//Std::copy
- #include <cstddef>//std::size_t
- Class Dumb_array
- {
- Public
- //(default) constructor
- Dumb_array (std::size_t size = 0):
- Msize (size),
- Marray (msize? new Int[msize] (): 0)
- {}
- //Copy-constructor
- Dumb_array (const dumb_array& Other):
- Msize (Other.msize),
- Marray (msize? new Int[msize]: 0),
- {
- //Note that this is non-throwing, because of the data
- //types being used; more attention to detail with regards
- //To exceptions must is given in a more general case, however
- Std::copy (Other.marray, Other.marray + msize, Marray);
- }
- //destructor
- ~dumb_array ()
- {
- Delete [] marray;
- }
- Private
- std::size_t msize;
- int* Marray;
- };
This class is almost as successful as the ability to manage dynamic classes, but requires opeator= to work properly.
Here is a less-than-good implementation:
[CPP]View Plaincopyprint?
- The hard part
- dumb_array& operator= (const dumb_array& Other)
- {
- if (This= &other) //(1)
- {
- //Get rid of the old data ...
- Delete [] marray; //(2)
- Marray = 0; //(2) * (see footnote for rationale)
- //... and put in the new
- Msize = other.msize; //(3)
- Marray = msize? new Int[msize]: 0; //(3)
- Std::copy (Other.marray, Other.marray + msize, Marray); //(3)
- }
- return * this;
- }
The above code has three questions, respectively, as indicated in parentheses.
(1) Self-assignment needs to be judged.
This discriminant has two purposes: a simple way to block redundant code, to prevent bugs (deleting arrays and then copying them). There are no problems at other times, just making the program slow. Self-assignment is relatively uncommon in programs, so this distinction is superfluous in most cases. In this way, it is better to work without this discriminant.
(2) Only basic exception safety guarantee is provided.
If new Int[msize] fails, then the *this is modified (the array size is wrong and the array is lost). To provide a strong guarantee, you need to do this:
[CPP]View Plaincopyprint?
- dumb_array& operator= (const dumb_array& Other)
- {
- if (This= &pother) //(1)
- {
- //Get the new data ready before we replace the old
- std::size_t newSize = other.msize;
- int* NewArray = newSize? new Int[newsize] (): 0; //(3)
- Std::copy (Other.marray, Other.marray + newSize, NewArray); //(3)
- //Replace the old data (all is non-throwing)
- Delete [] marray;
- Msize = newSize;
- Marray = NewArray;
- }
- return * this;
- }
The code is bloated! This leads to another problem:
(3) Code redundancy.
The core code is allocated space and copies in only two lines. If you want to implement more complex resource management, then the expansion of the code will lead to very serious problems.
3. A successful solution
As mentioned earlier, Copy-and-swap can solve all of these problems. But now, there's another thing we need to do: the swap function. The rules "the rule of three" indicate the existence of copy constructors, assignment operators, and destructors. In fact, it should be called "the Big and half": any time your class wants to manage a resource, it is necessary to provide a swap function.
We need to add a swap function to our class to see the following code:
[CPP]View Plaincopyprint?
- Class Dumb_array
- {
- Public
- // ...
- friend void Swap (dumb_array& first, dumb_array& second) //nothrow
- {
- //Enable ADL (not necessary in our case, but good practice)
- using Std::swap;
- //By swapping the members of the classes,
- //The classes is effectively swapped
- Swap (first.msize, second.msize);
- Swap (First.marray, second.marray);
- }
- // ...
- };
Now we can not only exchange Dumb_array, but also exchange is very efficient: it simply swaps pointers and arrays, rather than reallocating space and copying the entire array.
In this way, we can implement the copy assignment operator as follows:
[CPP]View Plaincopyprint?
- dumb_array& operator= (Dumb_array Other) //(1)
- {
- Swap (* This, other); //(2)
- return * this;
- }
That's it! All of the three issues mentioned above have been resolved.
4, why can work properly
We have noticed a very important detail: parameters are passed by value.
Some people may easily do this (in fact, many failed implementations do):
[CPP]View Plaincopyprint?
- dumb_array& operator= (const dumb_array& Other)
- {
- Dumb_array temp (other);
- Swap (* This, temp);
- return * this;
- }
In doing so, we will lose an important optimization opportunity (refer to want speed?). Pass by Value). And in the c++11, it is highly controversial.
generally, we'd better follow the more useful rules: don't copy function parameters. You should pass the parameters by value, allowing the compiler to complete the copy work.
This way of managing resources solves the problem of code redundancy, and we can use the copy constructor to complete the copy function without a bitwise copy. Once the copy function is complete, we are ready to swap.
Notice that once it enters the function body, all new data has been allocated, copied, and can be used. This provides a strong exception security guarantee: If the copy fails, we do not enter into the function body, then the content pointed to by this pointer will not be changed. (in front of what we did to implement a strong guarantee, now the compiler did it for us).
The swap function is non-throwing. We exchange the old data with the new data, change our state safely, and the old data is put into the temporary object. This way, when the function exits, the old data is automatically freed.
Because Copy-and-swap does not have code redundancy, we do not introduce bugs in this operator. We have also avoided self-assignment testing.
"In-depth exploration of C + +" Copy-and-swap idiom understand and implement safe self-assignment