📄 mi17.htm
字号:
More Effective C++ | Item 17: Consider using lazy evaluation Back to Item 16: Remember the 80-20 ruleContinue to Item 18: Amortize the cost of expected computationsItem 17: Consider using lazy evaluation.From the perspective of efficiency, the best computations are those you never perform at all. That's fine, but if you don't need to do something, why would you put code in your program to do it in the first place? And if you do need to do something, how can you possibly avoid executing the code that does it?The key is to be lazy.Remember when you were a child and your parents told you to clean your room? If you were anything like me, you'd say "Okay," then promptly go back to what you were doing. You would not clean your room. In fact, cleaning your room would be the last thing on your mind until you heard your parents coming down the hall to confirm that your room had, in fact, been cleaned. Then you'd sprint to your room and get to work as fast as you possibly could. If you were lucky, your parents would never check, and you'd avoid all the work cleaning your room normally entails.It turns out that the same delay tactics that work for a five year old work for a C++ programmer. In Computer Science, however, we dignify such procrastination with the name lazy evaluation. When you employ lazy evaluation, you write your classes in such a way that they defer computations until the results of those computations are required. If the results are never required, the computations are never performed, and neither your software's clients nor your parents are any the wiser.Perhaps you're wondering exactly what I'm talking about. Perhaps an example would help. Well, lazy evaluation is applicable in an enormous variety of application areas, so I'll describe four.Reference CountingConsider this code: class String { ... }; // a string class (the standard // string type may be implemented // as described below, but it // doesn't have to be)String s1 = "Hello";String s2 = s1; // call String copy ctorA common implementation for the String copy constructor would result in s1 and s2 each having its own copy of "Hello" after s2 is initialized with s1. Such a copy constructor would incur a relatively large expense, because it would have to make a copy of s1's value to give to s2, and that would typically entail allocating heap memory via the new operator (see Item 8) and calling strcpy to copy the data in s1 into the memory allocated by s2. This is eager evaluation: making a copy of s1 and putting it into s2 just because the String copy constructor was called. At this point, however, there has been no real need for s2 to have a copy of the value, because s2 hasn't been used yet.The lazy approach is a lot less work. Instead of giving s2 a copy of s1's value, we have s2 share s1's value. All we have to do is a little bookkeeping so we know who's sharing what, and in return we save the cost of a call to new and the expense of copying anything. The fact that s1 and s2 are sharing a data structure is transparent to clients, and it certainly makes no difference in statements like the following, because they only read values, they don't write them: cout << s1; // read s1's valuecout << s1 + s2; // read s1's and s2's valuesIn fact, the only time the sharing of values makes a difference is when one or the other string is modified; then it's important that only one string be changed, not both. In this statement, s2.convertToUpperCase();it's crucial that only s2's value be changed, not s1's also.To handle statements like this, we have to implement String's convertToUpperCase function so that it makes a copy of s2's value and makes that value private to s2 before modifying it. Inside convertToUpperCase, we can be lazy no longer: we have to make a copy of s2's (shared) value for s2's private use. On the other hand, if s2 is never modified, we never have to make a private copy of its value. It can continue to share a value as long as it exists. If we're lucky, s2 will never be modified, in which case we'll never have to expend the effort to give it its own value.The details on making this kind of value sharing work (including all the code) are provided in Item 29, but the idea is lazy evaluation: don't bother to make a copy of something until you really need one. Instead, be lazy use someone else's copy as long as you can get away with it. In some application areas, you can often get away with it forever.Distinguishing Reads from WritesPursuing the example of reference-counting strings a bit further, we come upon a second way in which lazy evaluation can help us. Consider this code: String s = "Homer's Iliad"; // Assume s is a // reference-counted string...cout << s[3]; // call operator[] to read s[3]s[3] = 'x'; // call operator[] to write s[3]The first call to operator[] is to read part of a string, but the second call is to perform a write. We'd like to be able to distinguish the read call from the write, because reading a reference-counted string is cheap, but writing to such a string may require splitting off a new copy of the string's value prior to the write.This puts us in a difficult implementation position. To achieve what we want, we need to do different things inside operator[] (depending on whether it's being called to perform a read or a write). How can we determine whether operator[] has been called in a read or a write context? The brutal truth is that we can't. By using lazy evaluation and proxy classes as described in Item 30, however, we can defer the decision on whether to take read actions or write actions until we can determine which is correct.Lazy FetchingAs a third example of lazy evaluation, imagine you've got a program that uses large objects containing many constituent fields. Such objects must persist across program runs, so they're stored in a database. Each object has a unique object identifier that can be used to retrieve the object from the database: class LargeObject { // large persistent objectspublic: LargeObject(ObjectID id); // restore object from disk const string& field1() const; // value of field 1 int field2() const; // value of field 2 double field3() const; // ... const string& field4() const; const string& field5() const; ...};Now consider the cost of restoring a LargeObject from disk: void restoreAndProcessObject(ObjectID id){ LargeObject object(id); // restore object ...}Because LargeObject instances are big, getting all the data for such an object might be a costly database operation, especially if the data must be retrieved from a remote database and pushed across a network. In some cases, the cost of reading all that data would be unnecessary. For example, consider this kind of application: void restoreAndProcessObject(ObjectID id){ LargeObject object(id); if (object.field2() == 0) { cout << "Object " << id << ": null field2.\n"; }}Here only the value of field2 is required, so any effort spent setting up the other fields is wasted.The lazy approach to this problem is to read no data from disk when a LargeObject object is created. Instead, only the "shell" of an object is created, and data is retrieved from the database only when that particular data is needed inside the object. Here's one way to implement this kind of "demand-paged" object initialization: class LargeObject {public: LargeObject(ObjectID id); const string& field1() const; int field2() const; double field3() const; const string& field4() const; ...private: ObjectID oid; mutable string *field1Value; // see below for a mutable int *field2Value; // discussion of "mutable" mutable double *field3Value; mutable string *field4Value; ...};LargeObject::LargeObject(ObjectID id)
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -