![]() |
![]() |
|||||||
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
|||
What's wrong with this code? Volume #1The assignment operatorThe assignment operator for the Foo class contains a common programming mistake. Assignment operators should almost always pass their arguments by const reference. A lot of programmers leave out the const part, and just pass by reference. When you forget the const keyword, the code will usually continue to compile and run correctly. However, the code example in this issue demonstrates a situation where const cannot be omitted. Recall that the original assignment operator looked like this: Foo& operator=( Foo &rhs) { cout << "assignment" << endl; return *this; } The compilation error occurred where an object was being assigned a return value from a function. foo1 = GetAFoo(); // generates compiler error So why does this assignment fail to compile, when the assignment to foo2 compiles without error? foo2 = foo1; // compiles OK The difference is that GetAFoo returns a Foo object by value. Return objects are temporary objects, and temporary objects are const objects. You cannot modify a temporary object. foo1 on the other hand, is not a temporary object, which means that it is not a const object. The assignment operator for Foo takes a non-const object by reference. The compiler will not pass const objects to a function that take references to non-const objects. Why? Because when you pass a value by reference, the function has the power to alter the original object. The compiler must ensure that const objects are not changed. Because the foo assignment operator did not list the const keyword, the compiler will not try to use it for temporary objects. Since no other suitable assignment operator is available that takes const parameters, the compiler issues an error. The solution to this problem is to change the assignment operator so it takes a constant reference. class Foo { public: ... Foo& operator=(const Foo &rhs) { cout << "assignment" << endl; return *this; } }; Once this change is made to the Foo class, the code example compiles and runs without error. Here is the output of the program after making this correction. Note that the comments were added to explain the output. default constructor // construct foo1 default constructor // construct foo2 default constructor // construct foo inside GetAFoo copy construction // copy foo to temporary return object destructor // destory local foo in GetAFoo assignment // assign temp return object to foo1 destructor // destory temp object assignment // assign foo1 to foo2 destructor // end of main, destroy foo1 destructor // and destroy foo2. In addition to fixing the compilation error, there are a couple of ways to streamline the code. The first change involves the GetAFoo function, which originally looked like this: Foo GetAFoo() { Foo foo; return foo; } As it stands, this code will construct two Foo objects. The first is the foo variable that you see in the code. The second object is the return object for the function. The foo variable is constructed with the default constructor for the Foo class. The temporary return object is created with the copy constructor. The debug statements from the program shows how and when these two objects are created and destroyed.
... default constructor // creates foo variable copy construction // construct temp return object destructor // destroy foo because it is out of scope assignment // call assignment operator and pass temp object destructor // delete temp object ... As this output shows, calling the GetAFoo function results in the construction and deletion of two Foo objects. Because GetAFoo does not use the local foo variable for anything, we can streamline the function by eliminating the variable. The new and improved GetAFoo function looks like this: Foo GetAFoo() { return Foo(); }
This code eliminates the temporary variable, the copy constructor that was needed to initialize it, and the destructor that was needed to deallocate it. The debug output from the streamlined function looks like this. ... default constructor // create return object with default constructor assignment // pass return object to assignment operator destructor // destroy return object ... There is one other place where the code can be improved. Notice how the main function creates and initializes the two Foo objects. int main() { Foo foo1; // default constructor Foo foo2; // default constructor foo1 = GetAFoo(); // Assignment foo2 = foo1; // Assignment return 0; } When main is structured like this, four function calls are needed to initialize two objects. When can reduce this to two calls if by eliminating the use of the default constructors. int main() { Foo foo1(GetAFoo()); // copy constructor Foo foo2(foo1); // copy constructor return 0; } Ironically, this code enhancement eliminates the use of the assignment operator, and the assignment operator was the original cause of our compiler errors. After making these improvements to the code, the final source should look like this: #include <iostream> using namespace std; class Foo { public: Foo() { cout << "default constructor" << endl; } Foo(const Foo &) { cout << "copy construction" << endl; } ~Foo() { cout << "destructor" << endl; } Foo& operator=(const Foo &rhs) { cout << "assignment" << endl; return *this; } }; Foo GetAFoo() { return Foo(); } int main() { Foo foo1(GetAFoo()); Foo foo2(foo1); return 0; } That does it for this edition of "What's wrong with this code?" Unless of course, you can find more ways to improve the final version of the code. Since this is the first edition of w3TC, I would be interested in your feedback. Do you like these sorts of articles, or did this article suck as much as the code that was in it? Feedback is welcome. If you have ideas for future articles, I would be happy to hear them. Just click the email link at the bottom of the page. | ||||||||
All rights reserved. |