📄 mi29.htm
字号:
More Effective C++ | Item 29: Reference counting Back to Item 28: Smart pointersContinue to Item 30: Proxy classesItem 29: Reference counting.Reference counting is a technique that allows multiple objects with the same value to share a single representation of that value. There are two common motivations for the technique. The first is to simplify the bookkeeping surrounding heap objects. Once an object is allocated by calling new, it's crucial to keep track of who owns that object, because the owner and only the owner is responsible for calling delete on it. But ownership can be transferred from object to object as a program runs (by passing pointers as parameters, for example), so keeping track of an object's ownership is hard work. Classes like auto_ptr (see Item 9) can help with this task, but experience has shown that most programs still fail to get it right. Reference counting eliminates the burden of tracking object ownership, because when an object employs reference counting, it owns itself. When nobody is using it any longer, it destroys itself automatically. Thus, reference counting constitutes a simple form of garbage collection.The second motivation for reference counting is simple common sense. If many objects have the same value, it's silly to store that value more than once. Instead, it's better to let all the objects with that value share its representation. Doing so not only saves memory, it also leads to faster-running programs, because there's no need to construct and destruct redundant copies of the same object value.Like most simple ideas, this one hovers above a sea of interesting details. God may or may not be in the details, but successful implementations of reference counting certainly are. Before delving into details, however, let us master basics. A good way to begin is by seeing how we might come to have many objects with the same value in the first place. Here's one way: class String { // the standard string type maypublic: // employ the techniques in this // Item, but that is not required String(const char *value = ""); String& operator=(const String& rhs); ...private: char *data;};String a, b, c, d, e;a = b = c = d = e = "Hello";It should be apparent that objects a through e all have the same value, namely "Hello". How that value is represented depends on how the String class is implemented, but a common implementation would have each String object carry its own copy of the value. For example, String's assignment operator might be implemented like this: String& String::operator=(const String& rhs){ if (this == &rhs) return *this; // see Item E17 delete [] data; data = new char[strlen(rhs.data) + 1]; strcpy(data, rhs.data); return *this; // see Item E15}Given this implementation, we can envision the five objects and their values as follows:The redundancy in this approach is clear. In an ideal world, we'd like to change the picture to look like this:Here only one copy of the value "Hello" is stored, and all the String objects with that value share its representation.In practice, it isn't possible to achieve this ideal, because we need to keep track of how many objects are sharing a value. If object a above is assigned a different value from "Hello", we can't destroy the value "Hello", because four other objects still need it. On the other hand, if only a single object had the value "Hello" and that object went out of scope, no object would have that value and we'd have to destroy the value to avoid a resource leak.The need to store information on the number of objects currently sharing referring to a value means our ideal picture must be modified somewhat to take into account the existence of a reference count:(Some people call this number a use count, but I am not one of them. C++ has enough idiosyncrasies of its own; the last thing it needs is terminological factionalism.)Implementing Reference CountingCreating a reference-counted String class isn't difficult, but it does require attention to detail, so we'll walk through the implementation of the most common member functions of such a class. Before we do that, however, it's important to recognize that we need a place to store the reference count for each String value. That place cannot be in a String object, because we need one reference count per string value, not one reference count per string object. That implies a coupling between values and reference counts, so we'll create a class to store reference counts and the values they track. We'll call this class StringValue, and because its only raison d'tre is to help implement the String class, we'll nest it inside String's private section. Furthermore, it will be convenient to give all the member functions of String full access to the StringValue data structure, so we'll declare StringValue to be a struct. This is a trick worth knowing: nesting a struct in the private part of a class is a convenient way to give access to the struct to all the members of the class, but to deny access to everybody else (except, of course, friends of the class).Our basic design looks like this: class String {public: ... // the usual String member // functions go hereprivate: struct StringValue { ... }; // holds a reference count // and a string value StringValue *value; // value of this String};We could give this class a different name (RCString, perhaps) to emphasize that it's implemented using reference counting, but the implementation of a class shouldn't be of concern to clients of that class. Rather, clients should interest themselves only in a class's public interface. Our reference-counting implementation of the String interface supports exactly the same operations as a non-reference-counted version, so why muddy the conceptual waters by embedding implementation decisions in the names of classes that correspond to abstract concepts? Why indeed? So we don't.Here's StringValue: class String {private: struct StringValue { int refCount; char *data; StringValue(const char *initValue); ~StringValue(); }; ...};String::StringValue::StringValue(const char *initValue): refCount(1){ data = new char[strlen(initValue) + 1]; strcpy(data, initValue);}String::StringValue::~StringValue(){ delete [] data;}That's all there is to it, and it should be clear that's nowhere near enough to implement the full functionality of a reference-counted string. For one thing, there's neither a copy constructor nor an assignment operator (see Item E11), and for another, there's no manipulation of the refCount field. Worry not the missing functionality will be provided by the String class. The primary purpose of StringValue is to give us a place to associate a particular value with a count of the number of String objects sharing that value. StringValue gives us that, and that's enough.We're now ready to walk our way through String's member functions. We'll begin with the constructors: class String {public: String(const char *initValue = ""); String(const String& rhs); ...};The first constructor is implemented about as simply as possible. We use the passed-in char* string to create a new StringValue object, then we make the String object we're constructing point to the newly-minted StringValue: String::String(const char *initValue): value(new StringValue(initValue)){}For client code that looks like this, String s("More Effective C++");we end up with a data structure that looks like this:String objects constructed separately, but with the same initial value do not share a data structure, so client code of this form, String s1("More Effective C++");String s2("More Effective C++");yields this data structure:It is possible to eliminate such duplication by having String (or StringValue) keep track of existing StringValue objects and create new ones only for truly unique strings, but such refinements on reference counting are somewhat off the beaten path. As a result, I'll leave them in the form of the feared and hated exercise for the reader.The String copy constructor is not only unfeared and unhated, it's also efficient: the newly created String object shares the same StringValue object as the String object that's being copied: String::String(const String& rhs): value(rhs.value){ ++value->refCount;}Graphically, code like this, String s1("More Effective C++");String s2 = s1;results in this data structure:This is substantially more efficient than a conventional (non-reference-counted) String class, because there is no need to allocate memory for the second copy of the string value, no need to deallocate that memory later, and no need to copy the value that would go in that memory. Instead, we merely copy a pointer and increment a reference count.The String destructor is also easy to implement, because most of the time it doesn't do anything. As long as the reference count for a StringValue is non-zero, at least one String object is using the value; it must therefore not be destroyed. Only when the String being destructed is the sole user of the value i.e., when the value's reference count is 1 should the String destructor destroy the StringValue object: class String {public: ~String(); ...};String::~String(){ if (--value->refCount == 0) delete value;}Compare the efficiency of this function with that of the destructor for a non-reference-counted implementation. Such a function would always call delete and would almost certainly have a nontrivial runtime cost. Provided that different String objects do in fact sometimes have the same values, the implementation above will sometimes do nothing more than decrement a counter and compare it to zero.If, at this point, the appeal of reference counting is not becoming apparent, you're just not paying attention.That's all there is to String construction and destruction, so we'll move on to consideration of the String assignment operator: class String {public: String& operator=(const String& rhs); ...};When a client writes code like this, s1 = s2; // s1 and s2 are both String objectsthe result of the assignment should be that s1 and s2 both point to the same StringValue object. That object's reference count should therefore be incremented during the assignment. Furthermore, the StringValue object that s1 pointed to prior to the assignment should have its reference count decremented, because s1 will no longer have that value. If s1 was the only String with that value, the value should be destroyed. In C++, all that looks like this: String& String::operator=(const String& rhs){ if (value == rhs.value) { // do nothing if the values return *this; // are already the same; this } // subsumes the usual test of // this against &rhs (see Item E17) if (--value->refCount == 0) { // destroy *this's value if delete value; // no one else is using it } value = rhs.value; // have *this share rhs's ++value->refCount; // value return *this;}Copy-on-WriteTo round out our examination of reference-counted strings, consider an array-bracket operator ([]), which allows individual characters within strings to be read and written: class String {public: const char& operator[](int index) const; // for const Strings char& operator[](int index); // for non-const Strings...};Implementation of the const version of this function is straightforward, because it's a read-only operation; the value of the string can't be affected: const char& String::operator[](int index) const{ return value->data[index];}(This function performs sanity checking on index in the grand C++ tradition, which is to say not at all. As usual, if you'd like a greater degree of parameter validation, it's easy to add.)The non-const version of operator[] is a completely different story. This function may be called to read a character, but it might be called to write one, too: String s;...cout << s[3]; // this is a reads[5] = 'x'; // this is a writeWe'd like to deal with reads and writes differently. A simple read can be dealt with in the same way as the const version of operator[] above, but a write must be implemented in quite a different fashion.When we modify a String's value, we have to be careful to avoid modifying the value of other String objects that happen to be sharing the same StringValue object. Unfortunately, there is no way for C++ compilers to tell us whether a particular use of operator[] is for a read or a write, so we must be pessimistic and assume that all calls to the non-const operator[] are for writes. (Proxy classes can help us differentiate reads from writes see Item 30.)To implement the non-const operator[] safely, we must ensure that no other String object shares the StringValue to be modified by the presumed write. In short, we must ensure that the reference count for a String's StringValue object is exactly one any time we return a reference to a character inside that StringValue object. Here's how we do it: char& String::operator[](int index){ // if we're sharing a value with other String objects, // break off a separate copy of the value for ourselves if (value->refCount > 1) { --value->refCount; // decrement current value's // refCount, because we won't // be using that value any more value = // make a copy of the new StringValue(value->data); // value for ourselves } // return a reference to a character inside our // unshared StringValue object return value->data[index];}
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -