|
|
|
|
Reference Counting
In the example above, the
copy-constructor and operator= make a new copy of what the pointer points
to, and the destructor deletes it. However, if your object requires a lot of
memory or a high initialization overhead, you may want to avoid this copying. A
common 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 if you want to write to the
object (the Dog in the example above)? More than one object may be using
this Dog, so you’d be modifying someone else’s Dog as
well as yours, which doesn’t seem very neighborly. To solve this
“aliasing” problem, an additional technique
called copy-on-write is used. Before writing to a
block of memory, you make sure no one else is using it. If the reference count
is greater than one, you must make yourself a 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:
//: C12:ReferenceCounting.cpp
// Reference count, copy-on-write
#include "../require.h"
#include <string>
#include <iostream>
using namespace std;
class Dog {
string nm;
int refcount;
Dog(const string& name)
: nm(name), refcount(1) {
cout << "Creating Dog: " << *this << endl;
}
// Prevent assignment:
Dog& operator=(const Dog& rv);
public:
// Dogs can only be created on the heap:
static Dog* make(const string& name) {
return new Dog(name);
}
Dog(const Dog& d)
: nm(d.nm + " copy"), refcount(1) {
cout << "Dog copy-constructor: "
<< *this << endl;
}
~Dog() {
cout << "Deleting Dog: " << *this << endl;
}
void attach() {
++refcount;
cout << "Attached Dog: " << *this << endl;
}
void detach() {
require(refcount != 0);
cout << "Detaching Dog: " << *this << endl;
// Destroy object if no one is using it:
if(--refcount == 0) delete this;
}
// Conditionally copy this Dog.
// Call before modifying the Dog, assign
// resulting pointer to your Dog*.
Dog* unalias() {
cout << "Unaliasing Dog: " << *this << endl;
// Don't duplicate if not aliased:
if(refcount == 1) return this;
--refcount;
// Use copy-constructor to duplicate:
return new Dog(*this);
}
void rename(const string& newName) {
nm = newName;
cout << "Dog renamed to: " << *this << endl;
}
friend ostream&
operator<<(ostream& os, const Dog& d) {
return os << "[" << d.nm << "], rc = "
<< d.refcount;
}
};
class DogHouse {
Dog* p;
string houseName;
public:
DogHouse(Dog* dog, const string& house)
: p(dog), houseName(house) {
cout << "Created DogHouse: "<< *this << endl;
}
DogHouse(const DogHouse& dh)
: p(dh.p),
houseName("copy-constructed " +
dh.houseName) {
p->attach();
cout << "DogHouse copy-constructor: "
<< *this << endl;
}
DogHouse& operator=(const DogHouse& dh) {
// Check for self-assignment:
if(&dh != this) {
houseName = dh.houseName + " assigned";
// Clean up what you're using first:
p->detach();
p = dh.p; // Like copy-constructor
p->attach();
}
cout << "DogHouse operator= : "
<< *this << endl;
return *this;
}
// Decrement refcount, conditionally destroy
~DogHouse() {
cout << "DogHouse destructor: "
<< *this << endl;
p->detach();
}
void renameHouse(const string& newName) {
houseName = newName;
}
void unalias() { p = p->unalias(); }
// Copy-on-write. Anytime you modify the
// contents of the pointer you must
// first unalias it:
void renameDog(const string& newName) {
unalias();
p->rename(newName);
}
// ... or when you allow someone else access:
Dog* getDog() {
unalias();
return p;
}
friend ostream&
operator<<(ostream& os, const DogHouse& dh) {
return os << "[" << dh.houseName
<< "] contains " << *dh.p;
}
};
int main() {
DogHouse
fidos(Dog::make("Fido"), "FidoHouse"),
spots(Dog::make("Spot"), "SpotHouse");
cout << "Entering copy-construction" << endl;
DogHouse bobs(fidos);
cout << "After copy-constructing bobs" << endl;
cout << "fidos:" << fidos << endl;
cout << "spots:" << spots << endl;
cout << "bobs:" << bobs << endl;
cout << "Entering spots = fidos" << endl;
spots = fidos;
cout << "After spots = fidos" << endl;
cout << "spots:" << spots << endl;
cout << "Entering self-assignment" << endl;
bobs = bobs;
cout << "After self-assignment" << endl;
cout << "bobs:" << bobs << endl;
// Comment out the following lines:
cout << "Entering rename(\"Bob\")" << endl;
bobs.getDog()->rename("Bob");
cout << "After rename(\"Bob\")" << endl;
} ///:~
The class Dog is the object
pointed to by a DogHouse. It contains a reference count and functions to
control and read the reference count. There’s a copy-constructor so you
can make a new Dog from an existing one.
The attach( ) function
increments the reference count of a Dog to indicate there’s another
object using it. detach( ) decrements the reference count. If the
reference count goes to zero, then no one is using it anymore, so the member
function destroys its own object by saying delete this.
Before you make any modifications (such
as renaming a Dog), you should ensure that you aren’t changing a
Dog that some other object is using. You do this by calling
DogHouse::unalias( ), which in turn calls
Dog::unalias( ). The latter function will return the existing
Dog pointer if the reference count is one (meaning no one else is
pointing to that Dog), but will duplicate the Dog if the reference
count is more than one.
The copy-constructor, instead of creating
its own memory, assigns Dog to the Dog of the source object. Then,
because there’s now an additional object using that block of memory, it
increments the reference count by calling
Dog::attach( ).
The operator= deals with an object
that has already been created on the left side of the =, so it must first
clean that up by calling detach( ) for that Dog, which will
destroy the old Dog if no one else is using it. Then operator=
repeats the behavior of the copy-constructor. Notice that it first checks to
detect whether you’re assigning the same object to
itself.
The destructor calls
detach( ) to conditionally destroy the Dog.
To implement copy-on-write, you must
control all the actions that write to your block of memory. For example, the
renameDog( ) member function allows you to change the values in the
block of memory. But first, it uses unalias( ) to prevent the
modification of an aliased Dog (a Dog with more than one
DogHouse object pointing to it). And if you need to produce a pointer to
a Dog from within a DogHouse, you unalias( ) that
pointer first.
main( ) tests the various
functions that must work correctly to implement reference counting: the
constructor, copy-constructor, operator=, and destructor. It also tests
the copy-on-write by calling renameDog( ).
Here’s the output (after a little
reformatting):
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 FidoHouse]
contains [Fido], rc = 2
After copy-constructing bobs
fidos:[FidoHouse] contains [Fido], rc = 2
spots:[SpotHouse] contains [Spot], rc = 1
bobs:[copy-constructed FidoHouse]
contains [Fido], rc = 2
Entering spots = fidos
Detaching Dog: [Spot], rc = 1
Deleting Dog: [Spot], rc = 0
Attached Dog: [Fido], rc = 3
DogHouse operator= : [FidoHouse assigned]
contains [Fido], rc = 3
After spots = fidos
spots:[FidoHouse assigned] contains [Fido],rc = 3
Entering self-assignment
DogHouse operator= : [copy-constructed FidoHouse]
contains [Fido], rc = 3
After self-assignment
bobs:[copy-constructed FidoHouse]
contains [Fido], rc = 3
Entering rename("Bob")
After rename("Bob")
DogHouse destructor: [copy-constructed FidoHouse]
contains [Fido], rc = 3
Detaching Dog: [Fido], rc = 3
DogHouse destructor: [FidoHouse assigned]
contains [Fido], rc = 2
Detaching Dog: [Fido], rc = 2
DogHouse destructor: [FidoHouse]
contains [Fido], rc = 1
Detaching Dog: [Fido], rc = 1
Deleting Dog: [Fido], rc = 0
By studying the output, tracing through
the source code, and experimenting with the program, you’ll deepen your
understanding of these techniques.
|
|
|