Sutter Assignment Operator Copy Constructor Example

How good are you at the details of writing classes? This item focuses not only on blatant errors, but even more so on professional style. Understanding these principles will help you to design classes that are easier to use and easier to maintain.

Problem

JG Question

1. What makes interfaces “easy to use correctly, hard to use incorrectly”? Explain.

Guru Question

2. You are doing a code review. A programmer has written the following class, which shows some poor style and has some real errors. How many can you find, and how would you fix them?

Note: This is not intended to be a complete class. For example, if you provide operator++ you would normally also provide operator–. Rather, this is an instructive example to focus on the mechanics of writing correctly the kinds of functions this class is trying to support.

Solution

1. What makes interfaces “easy to use correctly, hard to use incorrectly”? Explain.

We want to enable a “pit of success” where users of our type just naturally fall into good practices—they just naturally write code that is valid, correct, and efficient.

On the other hand, we want to make it hard for our users to get into trouble—we want code that would be incorrect or inefficient to be invalid (a compile time error if possible) or at least inconvenient and hard to write silently so that we can protect the user from unwelcome surprises.

Scott Meyers popularized this guidance. See his concise writeup for further examples.

2. You are doing a code review. A programmer has written the following class, which shows some poor style and has some real errors. How many can you find, and how would you fix them?

This class has a lot of problems—even more than I will show explicitly here. The point of this puzzle was primarily to highlight class mechanics (issues like “what is the canonical form of operator<<?” and “should operator+ be a member?”) rather than point out where the interface is just plain poorly designed. However, I will start off with perhaps the two most useful observation first:

First, this is a code review but the developer doesn’t seem to have tried to even unit-test his code, else he would have found some glaring problems.

Second, why write a complex class when one already exists in the standard library? And, what’s more, when the standard one isn’t plagued with any of the following problems and has been crafted based on years of practice by the best people in our industry? Humble thyself and reuse.

Guideline: Reuse code—especially standard library code—instead of handcrafting your own. It’s faster, easier, and safer.

Perhaps the best way to fix the problems in the complex code is to avoid using the class at all, and use the std::complex template instead.

Having said that, it’s an instructive example, so let’s go through the class as written and fix the problems as we go. First, the constructor:

1. The default constructor is missing.

Once we supply a user-written constructor, we suppress the implicit generation of the default constructor. Beyond “easy to use correctly,” not having a default constructor makes the class annoying to use at all. In this case, we could either default both parameters, or provide a complex() = default; and declare the data members with initializers such as double real = 0, imag = 0; , or just delegate with complex() : complex(0) { } . Just defaulting the parameter is the simplest here.

Also, as explained in GotW #1, prefer to use { } consistently for initialization rather than ( ) just as a good modern habit. The two mean exactly the same thing in this case, but { } lets us be more consistent, and could catch a few errors during maintenance, such as typos that would invoke double-to-float narrowing conversions.

2. operator+ passes by value.

Although we’re about make other changes to this function in a moment, as written this parameter should be passed by const& because all we do is read from it.

Guideline: Prefer passing a read-only parameter by const& if you are only going to read from it (not make a copy of it).

3. operator+ modifies this object’s value.

Instead of returning void, operator+ should return a complex containing the sum and not modify this object’s value. Users who write val1 + val2 and see val1 changed are unlikely to be impressed by these gratuitously weird semantics. As Scott Meyers is wont to say, when writing a value type, “do as the ints do” and follow the conventions of the built-in types.

4. operator+ is not written in terms of operator+= (which is missing).

Really, this operator+ is trying to be operator+=. It should be split into an actual operator+ and operator+=, with the former calling the latter.

Guideline: If you supply a standalone version of an operator (e.g., operator+), always supply an assignment version of the same operator (e.g., operator+=) and prefer implementing the former in terms of the latter. Also, always preserve the natural relationship between op and op= (where op stands for any operator).

Having += is good, because users should prefer using it. Even in the above code, real = real + other.real; should be real += other.real; and similarly for the second line.

Guideline: Prefer writing a op= b instead of a = a op b (where op stands for any operator). It’s clearer, and it’s often more efficient.

The reason why operator+= is more efficient is that it operates on the left-hand object directly and returns only a reference, not a temporary object. On the other hand, operator+ must return a temporary object. To see why, consider the following canonical forms for how operator+= and operator+ should normally be implemented for some type T.

Did you notice that one parameter is passed by value, and one by reference? That’s because if you’re going to copy from a parameter anyway, it’s often better to pass it by value, which will naturally enable a move operation if the caller passes a temporary object such as in expressions like (val1 * val2) + val3. This is a good habit to follow even in cases like complex where a move is the same cost as a copy, since it doesn’t cost any efficiency when move and copy are the same, and arguably makes for cleaner code than passing by reference and adding an extra named local object. We’ll see more on parameter passing in a future GotW.

Guideline: Prefer passing a read-only parameter by value if you’re going to make a copy of the parameter anyway, because it enables move from rvalue arguments.

Implementing + in terms of += both makes the code simpler and guarantees consistent semantics as the two functions are less likely to diverge during maintenance.

5. operator+ should not be a member function.

If operator+ is made a member function, as it is here, then it won’t work as naturally as your users may expect when you do decide to allow implicit conversions from other types. Here, an implicit conversion from double to complex makes sense, but with the original class users have an asymmetry: Specifically, when adding complex objects to numeric values, you can write a = b + 1.0 but not a = 1.0 + b because a member operator+ requires a complex (and not a double) as its left-hand argument.

Finally, the other reason to prefer non-members is because they provide better encapsulation, as pointed out by Scott Meyers.

Guideline: Prefer these guidelines for making an operator a member vs. nonmember function: unary operators are members; = () [] and -> must be members; the assignment operators (+= –= /= *= etc.) must be members; all other binary operators are nonmembers.

6. operator<< should not be a member function.

The author of this code didn’t really mean to enable the syntax my_complex << cout, did they?

The same reasons already given to show why operator+ should be a nonmember apply also to operator<<, only more so because a member the first parameter has to be a stream, not a complex. Further, the parameters should be references: (ostream&, const complex &).

Note also that the nonmember operator<< should normally be implemented in terms of a(n often virtual) const member function that does the work, usually named something like print.

7. operator<< should return ostream&.

Further, operator<< should have a return type of ostream& and should return a reference to the stream in order to permit chaining. That way, users can use your operator<< naturally in code like cout << a << b;.

Guideline: Always return stream references from operator<< and operator>>.

8. The preincrement operator’s return type is incorrect.

Ignoring for the sake of argument whether preincrement is meaningful for complex numbers, if the function exists it should return a reference. This lets client code operate more intuitively and avoids needless inefficiency.

Guideline: When you return *this, the return type should usually be a reference.

9. Postincrement should be implemented in terms of preincrement.

Instead of repeating the work, prefer to call ++*this. See GotW #2 for the full canonical form for postincrement.

Guideline: For consistency, always implement postincrement in terms of preincrement, otherwise your users will get surprising (and often unpleasant) results.

Summary

That’s it. There are other modern C++ features we could apply here, but they would be arguably gratuitous and not appropriate for general recommendations. For example, this is a value type not designed to be inherited from, so we could prevent inheritance by making the class final, but that would be protecting against Machiavelli, not Murphy, and there’s no need for a general guideline that tells everyone they should now write final on every value type; that would just be tedious and unnecessary.

Here’s a corrected version of the class, ignoring design and style issues not explicitly noted above:

Acknowledgments

Thanks in particular to the following for their feedback to improve this article: Mikhail Belyaev, jlehrer, Olaf van der Spek, Marshall, litb1, hm, Dave Harris, nosenseetal.

Posted in GotW | 39 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.[1] For example:

My_Arrayfirst;// initialization by default constructorMy_Arraysecond(first);// initialization by copy constructorMy_Arraythird=first;// Also initialization by copy constructorsecond=third;// assignment by copy assignment operator

Return value of overloaded assignment operator[edit]

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.[2]

Overloading copy assignment operator[edit]

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:

  1. Acquire new resources
  2. Release old resources
  3. Assign the new resources' handles to the object
classMy_Array{int*array;intcount;public:My_Array&operator=(constMy_Array&other){if(this!=&other)// protect against invalid self-assignment{// 1: allocate new memory and copy the elementsint*new_array=newint[other.count];std::copy(other.array,other.array+other.count,new_array);// 2: deallocate old memorydelete[]array;// 3: assign the new memory to the objectarray=new_array;count=other.count;}// by convention, always return *thisreturn*this;}// ...};

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:[3]

public:voidswap(My_Array&other)// the swap member function (should never fail!){// swap all the members (and base subobject, if applicable) with otherusingstd::swap;// because of ADL the compiler will use // custom swap for members if it exists// falling back to std::swapswap(array,other.array);swap(count,other.count);}My_Array&operator=(My_Arrayother)// note: argument passed by value!{// swap this with otherswap(other);// by convention, always return *thisreturn*this;// other is destroyed, releasing the memory}

Assignment between different classes[edit]

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:

classAncestor{public:inta;};classDescendant:publicAncestor{public:intb;};intmain(){Descendantd;Ancestora(d);Ancestorb(d);a=d;return0;}

Copying from ancestor to descendant objects, which could leave descendant's fields uninitialized, is not permitted.

See also[edit]

References[edit]

External links[edit]

  1. ^Stroustrup, Bjarne (2000). The C++ Programming Language (3 ed.). Addison-Wesley. p. 244. ISBN 978-0-201-70073-2. 
  2. ^Working Draft, Standard for Programming Language C++, Section 17.6.3.1, Table 23; http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf
  3. ^Sutter, H.; Alexandrescu, A. (October 2004), C++ Coding Standards, Addison-Wesley, ISBN 0-321-11358-6 
Categories: 1

0 Replies to “Sutter Assignment Operator Copy Constructor Example”

Leave a comment

L'indirizzo email non verrà pubblicato. I campi obbligatori sono contrassegnati *