Before we move into discussing move semantics (the next step on my quest to convert you all to teaching in C++14), let’s clear up something that I often see being taught in a subtly “wrong” way with respect to memory management in even C++98.
Consider the following class, which might be written by a student in a fundamental data structures course:
What we see here is standard fare: we have a class that utilizes dynamic memory (via the pointer), and thus is provides the “Big Three”: a copy constructor, an assignment operator, and a destructor. These three overloads give us the value semantics that we desire, making it so that our class can “blend in” with the built in types of the language and standard library. C++ is rooted in value semantics, so it’s crucial that we get this right so that our classes are well behaved.
But let’s look more closely at our assignment operator. You may have seen it written something like this:
where is a helper function for releasing the memory associated with the current object, and is another helper function that is responsible for creating the memory for a new independent copy of the parameter, assigning it into the current object.
There are actually a couple of problems with this approach.
Because the language semantics are often taught very early on in the course, and (at least at UIUC) to fairly inexperienced programmers (through no fault of their own: it’s just their second programming experience in the curriculum in its current form), you have to dance around this issue of the “self assignment” check.
: an enigma for “green” students
is particularly nuanced for students still struggling to understand some of the fundamental differences between Java and C++ (or C and C++). This requires them to understand:
the type of (a pointer to the current instance)
the purpose of as getting a pointer to the argument (not a reference, and understanding that itself is not a pointer)
what it would mean if .
That’s quite a bit of information we’re expecting them to digest in just a short little snippet. But if you write your assignment operator this way, it’s such a critical moment: if they forget this check, they will have memory errors coming out of their ears.
However, that’s not the real meat of my argument. My real beef with this setup is that it is completely exception unsafe. And, unless you’re living in a fairytale world where you
- never allocate to the free store, and
- never use the standard library
ignoring exceptions will be a fatal mistake at some point in your experience with C++.
And, please don’t come to me claiming that your codebase “doesn’t throw exceptions”, because you and I both know that’s just a load of crock. =)
Nearly every useful program is going to at least do one (and, likely, both) of the above two things. This means you have to care about exceptions.
So what’s “unsafe” about this code?
Patience, young padawan. Let’s take a step back.
First, let’s identify where exceptions could be thrown, and then define what we want our class to do in the event of an exception. This will define what kind of “exception safety guarantee” we want to provide.
One of the bullet points I made above (when I was being rude; sorry) was that the memory allocator can throw exceptions. How could that be the case? Let’s look at three fairly simple examples:
We’re out of memory. This causes a exception to be thrown from the call to that we’ll be using to allocate our array.
A constructor for an element in the array throws during the call.
The assignment operator for an element in the new array throws when we are copying the data.
So clearly, then, the line that invokes has the potential to throw an exception. What would happen to our data structure in this case? There are a few cases:
It could be completely empty if the allocation itself fails (out of memory or a throwing constructor for during the call).
It could be partially-copied if the exception came from when copying the data.
So what can we do to deal with this exception?
Let me be clear here: our goal is not to handle the exception. What should the program do if it can no longer allocate heap memory, for example? That’s not something that our data structure should be deciding. So we’re not even going to try to catch and handle this error: instead, what we’re going to try to guarantee is something about the state of our class after the exception has been thrown—namely, that it is in the same state as it was before the assignment line that caused the exception.
Putting the safety back on our assignment operator
Using the template we had before, we could imagine rewriting it in the following way:
Shield your eyes! The horror! The code has exploded, has a horrible block to handle the fact that could throw during the assignment into the array (the cost of a generic type here), and is now almost certainly above the threshold of beginner programmers.
But it is exception safe.
Back to the drawing board
The above monstrosity is clearly beyond what we want to teach. There’s no reason we shouldn’t be able to achieve both goals: the ease of understanding that came with the then version, and also providing the strong exception safety guarantee.
This is where the “copy and swap” idiom comes into play. (It’s worth noting that this idiom is even more useful in C++11/14, but we’ll get there later.)
We start with the following observations:
We want to create new memory that is a completely independent copy of the parameter.
We must release the memory associated with the current object.
The current object must refer to the new memory that we’ve created.
…what if I told you that we already wrote most of this by virtue of having a well defined class? A helper? We have a copy constructor! Let’s see if we can’t use that as a form of “helper function”. Remember from the above code that we want the following chain of events:
- Allocate memory
- Copy over values
- Free old memory
- Refer to the new copy
and further note that there’s no reason we couldn’t do the last two in a different order (we’d just need a temporary).
Let’s first define a helper function that we’ll use in our implementation:
To be a good citizen, let’s also define a non-member swap function for our class that just delegates to the helper above:
And now consider the following implementation for the assignment operator:
Woah! We have two lines of code. There’s no way that gets us everything we need… right?
But it does.
We get the copy by virtue of the argument being passed by value.
If the copying fails (e.g., the copy constructor throws), our function is not run at all, so our class appears unchanged by the assignment because it truly didn’t happen.
Swapping with the value parameter accomplishes our resource release. Remember that any parameters passed by value are going to have their destructors invoked when the stack frame for that function is popped.
We don’t have to check for self assignment anymore, as that code path can now be handled without a special case. Yes, it is less efficient, but the point of checking for self assignment wasn’t as an optimization, it was a “please don’t crash my program” check.
The only thing we have to guarantee now is that our copy constructor satisfies the basic exception guarantee (which is to say that it does not leak in the event of an exception), which isn’t too bad (though the code is still not ideal):
The nastiness here is because the marked line (1) could throw during ’s assignment operator.
In the general case, there are ways of avoiding the here, but I think this is a reasonable compromise for now. It’s worth noting at this point that if you were teaching with the then style, your copy constructor was probably exception unsafe, too, so this isn’t just a reflection of some “complication” in the copy-and-swap idiom.
If you’re dealing with some type that you know does not throw from its assignment operator (an assumption I’m willing to make when teaching novice programmers), then the code can be simplified to just:
We’ll revisit this later when we start talking about C++11/14 and show how just a simple language switch can ensure that we get the basic exception guarantee out of our copy constructor in the general case by only a one line change to the above initializer list!
Closing Thoughts: An exception-safe “Big Three” for intro programmers
Let’s recap what our code for the “Big Three” looks like now, including all of our helper functions:
Real world applicability: exceptions are everwhere, you need to know them and how to handle them
Simplified explanation for , using language concepts they’re learning as they are doing copy constructors anyway (pass by value)
Elimination of the self assignment check (self assignment is automatically valid in the copy-and-swap idiom)
A helper function that’s useful to the outside world:
Requires some discussion of what exceptions are, what they are used for, and why we care about them
If you are truly being careful, in C++98/03 you will need to have a block in the copy constructor (but not in C++11/14, more to come…)
Now that we know about the copy-and-swap idiom, in the next post I’m going to talk briefly about move semantics in C++11/14, and then we can move on to tackle what I teased at in the very first post in this series: that we can teach manual memory management in C++11/14 without losing out on any teaching opportunities compared to C++98/03, all the while simultaneously being more modern and encouraging students to write safe code.
Yell at me in the comments!
In the C++programming language, the assignment operator, , is the operator used for assignment. Like most other operators in C++, it can be overloaded.
The copy assignment operator, often just called the "assignment operator", is a special case of assignment operator where the source (right-hand side) and destination (left-hand side) are of the same class type. It is one of the special member functions, which means that a default version of it is generated automatically by the compiler if the programmer does not declare one. The default version performs a memberwise copy, where each member is copied by its own copy assignment operator (which may also be programmer-declared or compiler-generated).
The copy assignment operator differs from the copy constructor in that it must clean up the data members of the assignment's target (and correctly handle self-assignment) whereas the copy constructor assigns values to uninitialized data members. For example:
Return value of overloaded assignment operator
The language permits an overloaded assignment operator to have an arbitrary return type (including ). However, the operator is usually defined to return a reference to the assignee. This is consistent with the behavior of assignment operator for built-in types (returning the assigned value) and allows for using the operator invocation as an expression, for instance in control statements or in chained assignment. Also, the C++ Standard Library requires this behavior for some user-supplied types.
Overloading copy assignment operator
When deep copies of objects have to be made, exception safety should be taken into consideration. One way to achieve this when resource deallocation never fails is:
- Acquire new resources
- Release old resources
- Assign the new resources' handles to the object
However, if a no-fail (no-throw) swap function is available for all the member subobjects and the class provides a copy constructor and destructor (which it should do according to the rule of three), the most straightforward way to implement copy assignment is as follows:
Assignment between different classes
C++ supports assignment between different classes, both via implicit copy constructor and assignment operator, if the destination instance class is the ancestor of the source instance class:
Copying from ancestor to descendant objects, which could leave descendant's fields uninitialized, is not permitted.
- ^Stroustrup, Bjarne (2000). The C++ Programming Language (3 ed.). Addison-Wesley. p. 244. ISBN 978-0-201-70073-2.
- ^Working Draft, Standard for Programming Language C++, Section 126.96.36.199, Table 23; http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf
- ^Sutter, H.; Alexandrescu, A. (October 2004), C++ Coding Standards, Addison-Wesley, ISBN 0-321-11358-6