Safely Copying an Object

Problem

You need the basic class copy operationscopy construction and assignmentto be exception-safe.

Solution

Employ the tactics discussed in Recipe 9.4 by doing everything that might throw first, then changing the object state with operations that can't throw only after the hazardous work is complete. Example 9-6 presents the Message class again, this time with the assignment operator and copy constructor defined.

Example 9-6. Exception-safe assignment and copy construction

#include 
#include 

const static int DEFAULT_BUF_SIZE = 3;
const static int MAX_SIZE = 4096;

class Message {

public:
 Message(int bufSize = DEFAULT_BUF_SIZE) :
 bufSize_(bufSize),
 initBufSize_(bufSize),
 msgSize_(0),
 key_("") {
 buf_ = new char[bufSize]; // Note: now this is in the body
 }

 ~Message( ) {
 delete[] buf_;
 }

 // Exception-safe copy ctor
 Message(const Message& orig) : 
 bufSize_(orig.bufSize_),
 initBufSize_(orig.initBufSize_),
 msgSize_(orig.msgSize_),
 key_(orig.key_) { // This can throw...

 buf_ = new char[orig.bufSize_]; // ...so can this
 copy(orig.buf_, orig.buf_+msgSize_, buf_); // This can't
 }

 // Exception-safe assignment, using the copy ctor
 Message& operator=(const Message& rhs) {

 Message tmp(rhs); // Copy construct a temporary
 swapInternals(tmp); // Swap members with it
 return(*this); // When we leave, tmp is destroyed, taking
 // the original data with it
 }

 const char* data( ) {
 return(buf_);
 }

private:
 void swapInternals(Message& msg) {
 // Since key_ is not a built-in data type it can throw,
 // so do it first.
 swap(key_, msg.key_);

 // If it hasn't thrown, then do all the primitives
 swap(bufSize_, msg.bufSize_);
 swap(initBufSize_, msg.initBufSize_);
 swap(msgSize_, msg.msgSize_);
 swap(buf_, msg.buf_);
 }
 int bufSize_;
 int initBufSize_;
 int msgSize_;
 char* buf_;
 string key_;
};

 

Discussion

The copy constructor and the private member swapInternals do all the work here. The copy constructor initializes the primitive members and one of the nonprimitive members in the initializer list. Then it allocates a new buffer and copies the data into it. Simple enough, but why do it in this order? You could argue that all the initialization goes in the initializer list, but doing so can open the door for subtle bugs.

For example, you may want to put the buffer allocation in the initializer list, like this:

Message(const Message& orig) : 
 bufSize_(orig.bufSize_),
 initBufSize_(orig.initBufSize_),
 msgSize_(orig.msgSize_),
 key_(orig.key_),
 buf_(new char[orig.bufSize_]) {
 copy(orig.buf_, orig.buf_+msgSize_, buf_);
}

You might expect that everything will be fine, because if the new in the buffer allocation fails, all the other fully constructed objects will be destroyed. But this behavior is not guaranteed, because the members are initialized in the order in which they are declared in the class header, not the order in which you list them in the initializer list. The order of the member declaration looks like this:

int bufSize_;
int initBufSize_;
int msgSize_;
char* buf_;
string key_;

As a result, buf_ will be initialized before key_. If the initialization of key_ tHRows something, buf_ will not be destroyed and you will have created a hunk of unreferenced memory. You can guard against this by using a TRy/catch block in the constructor (see Recipe 9.2), but it is easier just to put buf_'s initialization in the body of the constructor where it is guaranteed to be called after the initializer list.

The call to copy won't throw because it's copying primitive values. But this is where the subtleties of exception-safety come in: it can throw if it is copying objects (e.g., if this is a generic container of T elements), in which case, you will need to catch it and delete the associated memory.

The other way you may want to copy an object is by using the assignment operator, operator=. Since it and the copy constructor have similar needs (e.g., make my members equal to my argument's members), reuse what you have already done and make your life easier. The only twist is that you can make things slick by using a private member to swap member data. I wish I had invented this technique, but I have to credit Herb Sutter and Stephen Dewhurst since their writing is where I first saw it.

It may make sense to you at first glance, but I will explain just in case it doesn't. Consider the first line, which copy constructs a temporary object, tmp:

Message tmp(rhs);

Now we have just created a clone of the object we are assigning from. Essentially, tmp is now equivalent to rhs. Now, swap its members with *this's members:

swapInternals(tmp);

I will come back to swapInternals in a moment. For now, all we care about is that now the *this's members are the same as tmp's were a second ago. And tmp was a copy of rhs, so now *this is equivalent to rhs. But wait: we still have this temporary object hanging around. No problem, when you return *this, tmp is automatically destroyed when it goes out of scope, taking the old members with it.

return(*this);

That's it. But is it exception-safe? Constructing tmp is, since our constructor is exception-safe. The call to swapInternals is what does the majority of the work, so let's have a look at what it does to see if it's safe.

swapInternals exchanges each data member in the current object with those in the object that is passed in. It does this by using swap, which takes two arguments a and b, creates a temporary copy of a, assigns b to a, and then assigns the temporary to b. As such, it is exception-safe and exception-neutral because the only exceptions that come out of it are those that may be thrown by the objects it is operating on. It uses no dynamic memory, so it upholds the basic guarantee of not leaking resources.

Since key_ isn't a primitive, which means that operations on it may throw an exception, I swap it first. That way, if it throws an exception, none of the other member variables are corrupted. This doesn't guarantee that key_ won't be corrupted though. When working with object members, you are at the mercy of their exception-safety guarantees. If that doesn't throw, I'm home free because I know that swapping native variables won't throw. Therefore, swapInternals is both basically and strongly exception-safe.

This brings up an interesting point though. What if you have more than one object member? If you had two string members, the beginning of swapInternals may look like this:

void swapInternals(Message& msg) {
 swap(key_, msg.key_);
 swap(myObj_, msg.myObj_);
 // ...

There is a problem: If the second swap throws an exception, how can we safely undo the first swap? In other words, now key_ has been updated with the new value, but the swap of myObj_ failed, so key_ is now corrupt. If the caller catches the exception and wants to proceed as though nothing happened, he is now working with something different than what he started with. Copying key_ to a temporary string first is one approach, but it can't guarantee safety because doing that copy may throw an exception.

One way to get around this is to use heap objects:

void swapInternals(Message& msg) {
 // key_ is a string* and myObj_ is a MyClass*
 swap(key_, msg.key_);
 swap(myObj_, msg.myObj_);

Of course, this means that now you have more dynamic memory to manage, but making exception-safety guarantees will often affect your design, so it is a good idea to start thinking about it early in the design process.

The theme for this recipe is unchanged from the previous recipes about exception-safety. Do the work that might cause problems first, wait with a try/catch block just in case something goes wrong, and, if something does go wrong, then clean up after yourself. If nothing goes wrong, pat yourself on the back and update the object state.

See Also

Recipe 9.2 and Recipe 9.3

Building C++ Applications

Code Organization

Numbers

Strings and Text

Dates and Times

Managing Data with Containers

Algorithms

Classes

Exceptions and Safety

Streams and Files

Science and Mathematics

Multithreading

Internationalization

XML

Miscellaneous

Index



C++ Cookbook
Secure Programming Cookbook for C and C++: Recipes for Cryptography, Authentication, Input Validation & More
ISBN: 0596003943
EAN: 2147483647
Year: 2006
Pages: 241

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net