Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 50 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
50
Dung lượng
171,41 KB
Nội dung
530 Thinking in C++ www.BruceEckel.com // Write the contents to an ostream: void print(std::ostream& os) const { os << "0x" << std::hex << int(b) << std::dec; } }; #endif // BYTE_H ///:~ //: C12:ByteTest.cpp #include "Byte.h" #include <fstream> using namespace std; ofstream out("ByteTest.out"); void k(Byte& b1, Byte& b2) { b1 = b1 * b2 + b2 % b1; #define TRY2(OP) \ out << "b1 = "; b1.print(out); \ out << ", b2 = "; b2.print(out); \ out << "; b1 " #OP " b2 produces "; \ (b1 OP b2).print(out); \ out << endl; b1 = 9; b2 = 47; TRY2(+) TRY2(-) TRY2(*) TRY2(/) TRY2(%) TRY2(^) TRY2(&) TRY2(|) TRY2(<<) TRY2(>>) TRY2(+=) TRY2(-=) TRY2(*=) TRY2(/=) TRY2(%=) TRY2(^=) TRY2(&=) TRY2(|=) TRY2(>>=) TRY2(<<=) TRY2(=) // Assignment operator // Conditionals: #define TRYC2(OP) \ out << "b1 = "; b1.print(out); \ out << ", b2 = "; b2.print(out); \ out << "; b1 " #OP " b2 produces "; \ out << (b1 OP b2); \ out << endl; b1 = 9; b2 = 47; TRYC2(<) TRYC2(>) TRYC2(==) TRYC2(!=) TRYC2(<=) TRYC2(>=) TRYC2(&&) TRYC2(||) // Chained assignment: Byte b3 = 92; 12: Operator Overloading 531 b1 = b2 = b3; } int main() { out << "member functions:" << endl; Byte b1(47), b2(9); k(b1, b2); } ///:~ You can see that operator= is only allowed to be a member function. This is explained later. Notice that all of the assignment operators have code to check for self-assignment; this is a general guideline. In some cases this is not necessary; for example, with operator+= you often want to say A+=A and have it add A to itself. The most important place to check for self-assignment is operator= because with complicated objects disastrous results may occur. (In some cases it’s OK, but you should always keep it in mind when writing operator= .) All of the operators shown in the previous two examples are overloaded to handle a single type. It’s also possible to overload operators to handle mixed types, so you can add apples to oranges, for example. Before you start on an exhaustive overloading of operators, however, you should look at the section on automatic type conversion later in this chapter. Often, a type conversion in the right place can save you a lot of overloaded operators. Arguments & return values It may seem a little confusing at first when you look at OverloadingUnaryOperators.cpp , Integer.h and Byte.h and see all the different ways that arguments are passed and returned. Although you can pass and return arguments any way you want to, the choices in these examples were not selected at random. They follow a logical pattern, the same one you’ll want to use in most of your choices. 532 Thinking in C++ www.BruceEckel.com 1. As with any function argument, if you only need to read from the argument and not change it, default to passing it as a const reference. Ordinary arithmetic operations (like + and – , etc.) and Booleans will not change their arguments, so pass by const reference is predominantly what you’ll use. When the function is a class member, this translates to making it a const member function. Only with the operator-assignments (like += ) and the operator= , which change the left-hand argument, is the left argument not a constant, but it’s still passed in as an address because it will be changed. 2. The type of return value you should select depends on the expected meaning of the operator. (Again, you can do anything you want with the arguments and return values.) If the effect of the operator is to produce a new value, you will need to generate a new object as the return value. For example, Integer::operator+ must produce an Integer object that is the sum of the operands. This object is returned by value as a const , so the result cannot be modified as an lvalue. 3. All the assignment operators modify the lvalue. To allow the result of the assignment to be used in chained expressions, like a=b=c , it’s expected that you will return a reference to that same lvalue that was just modified. But should this reference be a const or non const ? Although you read a=b=c from left to right, the compiler parses it from right to left, so you’re not forced to return a non const to support assignment chaining. However, people do sometimes expect to be able to perform an operation on the thing that was just assigned to, such as (a=b).func( ); to call func( ) on a after assigning b to it. Thus, the return value for all of the assignment operators should be a non const reference to the lvalue. 4. For the logical operators, everyone expects to get at worst an int back, and at best a bool . (Libraries developed before most 12: Operator Overloading 533 compilers supported C++’s built-in bool will use int or an equivalent typedef .) The increment and decrement operators present a dilemma because of the pre- and postfix versions. Both versions change the object and so cannot treat the object as a const . The prefix version returns the value of the object after it was changed, so you expect to get back the object that was changed. Thus, with prefix you can just return *this as a reference. The postfix version is supposed to return the value before the value is changed, so you’re forced to create a separate object to represent that value and return it. So with postfix you must return by value if you want to preserve the expected meaning. (Note that you’ll sometimes find the increment and decrement operators returning an int or bool to indicate, for example, whether an object designed to move through a list is at the end of that list.) Now the question is: Should these be returned as const or non const ? If you allow the object to be modified and someone writes (++a).func( ) , func( ) will be operating on a itself, but with (a++).func( ) , func( ) operates on the temporary object returned by the postfix operator++ . Temporary objects are automatically const , so this would be flagged by the compiler, but for consistency’s sake it may make more sense to make them both const , as was done here. Or you may choose to make the prefix version non- const and the postfix const . Because of the variety of meanings you may want to give the increment and decrement operators, they will need to be considered on a case-by-case basis. Return by value as const Returning by value as a const can seem a bit subtle at first, so it deserves a bit more explanation. Consider the binary operator+ . If you use it in an expression such as f(a+b) , the result of a+b becomes a temporary object that is used in the call to f( ) . Because it’s a temporary, it’s automatically const , so whether you explicitly make the return value const or not has no effect. However, it’s also possible for you to send a message to the return value of a+b , rather than just passing it to a function. For example, 534 Thinking in C++ www.BruceEckel.com you can say (a+b).g( ) , in which g( ) is some member function of Integer , in this case. By making the return value const , you state that only a const member function can be called for that return value. This is const -correct, because it prevents you from storing potentially valuable information in an object that will most likely be lost. The return optimization When new objects are created to return by value, notice the form used. In operator+ , for example: return Integer(left.i + right.i); This may look at first like a “function call to a constructor,” but it’s not. The syntax is that of a temporary object; the statement says “make a temporary Integer object and return it.” Because of this, you might think that the result is the same as creating a named local object and returning that. However, it’s quite different. If you were to say instead: Integer tmp(left.i + right.i); return tmp; three things will happen. First, the tmp object is created including its constructor call. Second, the copy-constructor copies the tmp to the location of the outside return value. Third, the destructor is called for tmp at the end of the scope. In contrast, the “returning a temporary” approach works quite differently. When the compiler sees you do this, it knows that you have no other need for the object it’s creating than to return it. The compiler takes advantage of this by building the object directly into the location of the outside return value. This requires only a single ordinary constructor call (no copy-constructor is necessary) and there’s no destructor call because you never actually create a local object. Thus, while it doesn’t cost anything but programmer awareness, it’s significantly more efficient. This is often called the return value optimization . 12: Operator Overloading 535 Unusual operators Several additional operators have a slightly different syntax for overloading. The subscript, operator[ ] , must be a member function and it requires a single argument. Because operator[ ] implies that the object it’s being called for acts like an array, you will often return a reference from this operator, so it can be conveniently used on the left-hand side of an equal sign. This operator is commonly overloaded; you’ll see examples in the rest of the book. The operators new and delete control dynamic storage allocation and can be overloaded in a number of different ways. This topic is covered in the Chapter 13. Operator comma The comma operator is called when it appears next to an object of the type the comma is defined for. However, “ operator, ” is not called for function argument lists, only for objects that are out in the open, separated by commas. There doesn’t seem to be a lot of practical uses for this operator; it’s in the language for consistency. Here’s an example showing how the comma function can be called when the comma appears before an object, as well as after: //: C12:OverloadingOperatorComma.cpp #include <iostream> using namespace std; class After { public: const After& operator,(const After&) const { cout << "After::operator,()" << endl; return *this; } }; class Before {}; Before& operator,(int, Before& b) { 536 Thinking in C++ www.BruceEckel.com cout << "Before::operator,()" << endl; return b; } int main() { After a, b; a, b; // Operator comma called Before c; 1, c; // Operator comma called } ///:~ The global function allows the comma to be placed before the object in question. The usage shown is fairly obscure and questionable. Although you would probably use a comma-separated list as part of a more complex expression, it’s too subtle to use in most situations. Operator-> The operator–> is generally used when you want to make an object appear to be a pointer. Since such an object has more “smarts” built into it than exist for a typical pointer, an object like this is often called a smart pointer . These are especially useful if you want to “wrap” a class around a pointer to make that pointer safe, or in the common usage of an iterator , which is an object that moves through a collection / container of other objects and selects them one at a time, without providing direct access to the implementation of the container. (You’ll often find containers and iterators in class libraries, such as in the Standard C++ Library, described in Volume 2 of this book.) A pointer dereference operator must be a member function. It has additional, atypical constraints: It must return an object (or reference to an object) that also has a pointer dereference operator, or it must return a pointer that can be used to select what the pointer dereference operator arrow is pointing at. Here’s a simple example: //: C12:SmartPointer.cpp 12: Operator Overloading 537 #include <iostream> #include <vector> #include " /require.h" using namespace std; class Obj { static int i, j; public: void f() const { cout << i++ << endl; } void g() const { cout << j++ << endl; } }; // Static member definitions: int Obj::i = 47; int Obj::j = 11; // Container: class ObjContainer { vector<Obj*> a; public: void add(Obj* obj) { a.push_back(obj); } friend class SmartPointer; }; class SmartPointer { ObjContainer& oc; int index; public: SmartPointer(ObjContainer& objc) : oc(objc) { index = 0; } // Return value indicates end of list: bool operator++() { // Prefix if(index >= oc.a.size()) return false; if(oc.a[++index] == 0) return false; return true; } bool operator++(int) { // Postfix return operator++(); // Use prefix version } Obj* operator->() const { require(oc.a[index] != 0, "Zero value " "returned by SmartPointer::operator->()"); return oc.a[index]; } 538 Thinking in C++ www.BruceEckel.com }; int main() { const int sz = 10; Obj o[sz]; ObjContainer oc; for(int i = 0; i < sz; i++) oc.add(&o[i]); // Fill it up SmartPointer sp(oc); // Create an iterator do { sp->f(); // Pointer dereference operator call sp->g(); } while(sp++); } ///:~ The class Obj defines the objects that are manipulated in this program. The functions f( ) and g( ) simply print out interesting values using static data members. Pointers to these objects are stored inside containers of type ObjContainer using its add( ) function. ObjContainer looks like an array of pointers, but you’ll notice there’s no way to get the pointers back out again. However, SmartPointer is declared as a friend class, so it has permission to look inside the container. The SmartPointer class looks very much like an intelligent pointer – you can move it forward using operator++ (you can also define an operator– – ), it won’t go past the end of the container it’s pointing to, and it produces (via the pointer dereference operator) the value it’s pointing to. Notice that the SmartPointer is a custom fit for the container it’s created for; unlike an ordinary pointer, there isn’t a “general purpose” smart pointer. You will learn more about the smart pointers called “iterators” in the last chapter of this book and in Volume 2 (downloadable from www.BruceEckel.com ). In main( ) , once the container oc is filled with Obj objects, a SmartPointer sp is created. The smart pointer calls happen in the expressions: sp->f(); // Smart pointer calls sp->g(); 12: Operator Overloading 539 Here, even though sp doesn’t actually have f( ) and g( ) member functions, the pointer dereference operator automatically calls those functions for the Obj* that is returned by SmartPointer::operator–> . The compiler performs all the checking to make sure the function call works properly. Although the underlying mechanics of the pointer dereference operator are more complex than the other operators, the goal is exactly the same: to provide a more convenient syntax for the users of your classes. A nested iterator It’s more common to see a “smart pointer” or “iterator” class nested within the class that it services. The previous example can be rewritten to nest SmartPointer inside ObjContainer like this: //: C12:NestedSmartPointer.cpp #include <iostream> #include <vector> #include " /require.h" using namespace std; class Obj { static int i, j; public: void f() { cout << i++ << endl; } void g() { cout << j++ << endl; } }; // Static member definitions: int Obj::i = 47; int Obj::j = 11; // Container: class ObjContainer { vector<Obj*> a; public: void add(Obj* obj) { a.push_back(obj); } class SmartPointer; friend SmartPointer; class SmartPointer { [...]... tests the copy-on-write by calling , renameDog( ) Here’s the output (after a little reformatting): 558 Thinking in C+ + www.BruceEckel.com Creating Dog: [Fido], rc = 1 Created DogHouse: [FidoHouse] contains [Fido], rc = 1 Creating Dog: [Spot], rc = 1 Created DogHouse: [SpotHouse] contains [Spot], rc = 1 Entering copy-construction Attached Dog: [Fido], rc = 2 DogHouse copy-constructor: [copy-constructed... approach to this problem is called reference counting You give intelligence to the object that’s being pointed to so it knows how many objects are pointing to it Then copy-construction or assignment means attaching another pointer to an existing object and incrementing the reference count Destruction means reducing the reference count and destroying the object if the reference count goes to zero But what... personal copy of that block before writing it, so you don’t disturb someone else’s turf Here’s a simple example of reference counting and copy-on-write: //: C1 2:ReferenceCounting.cpp // Reference count, copy-on-write #include " /require.h" #include #include using namespace std; class Dog { string nm; int refcount; Dog(const string& name) : nm(name), refcount(1) { cout ()"); return oc.a[index]; } }; // Function to produce a smart pointer that // points to the beginning of the ObjContainer: SmartPointer begin() { return SmartPointer(*this); } }; int main() { const int sz = 10; Obj o[sz]; ObjContainer oc; for(int i = 0; i < sz; i++) oc.add(&o[i]); // Fill it up ObjContainer::SmartPointer sp = oc.begin(); do { sp->f(); // Pointer dereference operator... dereferenced for the object In the following example, the FunctionObjectconstructor captures and stores both the pointer to the object and the pointer to the member function, and then the operator( )uses those to make the actual pointer-to-member call: //: C1 2:PointerToMemberOperator.cpp #include using namespace std; class Dog { public: int run(int i) const { cout . is called (which happens automatically), the 542 Thinking in C+ + www.BruceEckel.com pointer-to-member will be dereferenced for the object. In the following example, the FunctionObject constructor. { a.push_back(obj); } friend class SmartPointer; }; class SmartPointer { ObjContainer& oc; int index; public: SmartPointer(ObjContainer& objc) : oc(objc) { index = 0; . vector<Obj*> a; public: void add(Obj* obj) { a.push_back(obj); } class SmartPointer; friend SmartPointer; class SmartPointer { 540 Thinking in C+ + www.BruceEckel.com ObjContainer&