The Localized Ownership pattern language makes it possible

Tom is an independent consultant, the author of C++ Programming Style (Addison-Wesley, 1992), and a columnist for C++ Report. This article is based on his PLoP '95 paper published in Pattern Languages of Program Design 2 (Addison-Wesley, 1996). Tom can be contacted at [email protected]

The lifetime of a dynamic object in C++--one allocated from heap memory (the free-store)--is managed explicitly by the program. The program must guarantee that each dynamic object is deleted when it is no longer needed, and certainly before it becomes garbage. (There is no garbage collection in standard C++, and few programs can afford to produce garbage.) For each dynamic allocation, a policy that determines the object's lifetime must be found and implemented.

Localized Ownership is a pattern language that tackles the management of dynamic object lifetimes in C++. It forms a sequence of patterns that address a range of design constraints. Early patterns in the sequence emphasize simplicity and locality of the responsible code. Later patterns offer greater flexibility at the expense of a more complex implementation that is dispersed more widely.

The simplest policies for managing a dynamic object's lifetime are those that localize the work within a single component, such as a function, object, or class. A simple solution that suffices is ideal. Unfortunately, attempts to localize lifetime policies are confounded by rich object architectures that require their objects to play multiple roles in cooperation with many collaborators. What ownership policies are applicable in these more complex contexts?

Lifetime. The lifetime of an object in C++ is the interval of time it exists by occupying memory. An object is created from one of three "storage classes:" automatic (local), static, and dynamic. The lifetimes of automatic and static objects are controlled implicitly by the execution of the program. The lifetime of an automatic object ends when execution exits its block. The lifetime of a static object continues until the end of the execution of the entire program. However, the lifetime of a dynamic object continues until the program explicitly destroys that object.

Creator. A dynamic object is created by the execution of a new expression. The entity that executes the new expression is the dynamic object's creator. The creator may be a (member) function, object, or class. The creator cannot be inferred from the source code alone. In general, a new expression that creates a dynamic object is executed by a member function; that member function executes on behalf of an object; and that object is an instance of a class. Therefore, the creator of the dynamic object might be deemed to be the function, object, or class. In the following code, the creator could be the member function f, the object executing A::f, or the class A.

void A::f() { . . . new Resource . . . }

Although the creator is determined by the intent of the programmer, the language constrains the choice. If the new expression is executed by a static member function, the creator is either the function or its class. (There is no object.) If the new expression is executed by a nonmember function, only the function may be the creator. (There is neither object nor class.)

Owner. The owner of a dynamic object is the function, object, or class that bears the responsibility for ending the lifetime of the dynamic object. Upon creation of a dynamic object, its creator becomes the owner. A dynamic object may acquire an owner other than its creator. A dynamic object may have more than one owner at a time.

Ownership does not imply exclusive access to the dynamic object. Other parts of the program may legitimately access the dynamic object, but that access must be consistent with the owner's policy. In particular, other parts of the program must guarantee not to attempt any access after the owner has deleted the object.

The Localized Ownership pattern language contains three primary patterns, of which the first is divided into three sub-cases. The three patterns are as follows:

Creator As Sole Owner (Function As Sole Owner, Object As Sole Owner, and Class As Sole Owner).

Sequence of Owners.

Shared Ownership.

Although Localized Ownership patterns might be used to manage other resources (such as files or database locks), they are presented specifically in terms of dynamic objects. There are two reasons for this. First, the focus on dynamic objects makes the discussion more concrete. Second, for most C++ programmers, the effort expended on managing dynamic objects dominates the effort expended on managing other resources.

Context. The creator of a dynamic object is in a position to fully determine the object's lifetime. A narrow, specific purpose for the dynamic object suggests that its creator may be able to control its lifetime. A narrow purpose does not imply that the object is short-lived or that the creator has exclusive access to the object.

Solution. Upon creation of the dynamic object, its creator becomes the owner. Ownership responsibility is never transferred; the creator must eventually dispose of the dynamic object. Other parts of the system must access the dynamic object according to the owner's policy. When the identity of the dynamic object passes through an interface (such as a pointer or reference as a function parameter or return value), constraints on the object's lifetime must be part of that interface.

The three types of creator result in the following three specializations of this pattern.

Context. An object with access restricted to the function that creates it, and to other functions whose execution is nested within that function's execution, is a candidate for sole ownership by the function in which it is created. The lifetime of the object can be established without considering the lifetimes of any other objects.

Solution. When the sole owner is a function, that function must eventually delete the object. The delete expression is coded explicitly in the function. If the dynamic object is required for only the current invocation of the function, the delete occurs before the function returns to its caller. If the dynamic object's lifetime spans many invocations, the function may retain the object from call to call using a local static pointer. Eventually, the function itself executes the delete when the object is no longer needed.

Examples. The creator function creates and deletes during a single invocation; see Listing One.

A function that holds a dynamic buffer from one call to the next is shown in Listing Two. On each call, the function determines if the current buffer is large enough, allocating a larger buffer when needed.

Context. The lifetime of the dynamic object cannot be tied to a single function, but its lifetime can be bounded by the lifetime of the object that creates it. Creation occurs in a nonstatic member function.

Solution. If the sole owner is an object, then the owner acquired the dynamic object by executing one of its member functions and must delete the dynamic object before the end of its own lifetime. The last opportunity the owner object has to delete the owned object is during the execution of the owner's destructor.

Examples. Stroustrup's "Resource Allocation is Initialization" is an example of Object As Sole Owner (see The C++ Programming Language, Second Edition, Addison-Wesley, 1991).

The "Cheshire Cat" technique (a term coined by John Carolan in "Constructing Bullet-Proof Classes," Proceedings C++ At Work, SIGS Publications, 1989) reduces unwanted, indirect header-file dependencies by moving the private members of a class into an auxiliary object, to which the class delegates its operations. Typically, the auxiliary structure is dynamically allocated in the constructor and deallocated in the destructor; see Listing Three.

If a communications Connection object encounters an error, it opens a Window to display pertinent information. The Connection object lazily defers creation of the Window until an error occurs. If instructed to clearErrors(), the Connection disposes of the Window if it has one. If the Connection still holds a Window at the end of its own lifetime, the destructor performs the delete; see Listing Four.

Context. Even though there is no single function or object to serve as the owner, the ownership responsibility can be localized to a single class. The function that creates the object is a static or nonstatic member function of the class.

Solution. All parts of the class collaborate to determine the lifetime of the owned object. The implementation of the ownership responsibility is distributed across the member functions as they execute on behalf of all objects of that class. Static member functions may also participate.

Example. A Query object provides end-user database services. Query objects are created and destroyed arbitrarily. All Query objects share a single DataBase server. To conserve resources, the DataBase object should exist only when there is at least one Query object. Collectively, the constructor and destructor of Query use static data members to implement this policy on behalf of the class; see Listing Five.

Context. No suitable creator that can assume on-going ownership responsibility can be found. For example, a factory creates objects on behalf of others. Once the object has been successfully created, the factory's ownership responsibility should end, but the created object's lifetime must not.

Solution. Ownership may be transferred from the creator to another owner. Ownership may then be further transferred any number of times. At each point of transfer, the outgoing owner relinquishes its ownership obligation, whereupon the incoming owner assumes it. The transfer of ownership must be considered as an explicit part of the interface between the two. Although the owner may change many times, at any particular time, ownership rests with exactly one owner.

Examples. The code in Listing Six shows ownership transfer from a factory to a function. The transfer could as easily be to an object or a class as the incoming owner.

The function in Listing Seven transfers ownership to a local auto_ptr object. The auto_ptr template is part of the draft C++ standard library (ANSI Document X3J16/95-0091, "Working Paper for Draft Proposed International Standard for Information Systems Programming Language C++," April 1995). The only purpose of an auto_ptr object is to assume ownership of a dynamic object. The auto_ptr destructor deletes the object. The advantage of transferring ownership to an automatic object is that its destructor will execute under almost all control-flow scenarios that leave the block.

Context. When a dynamic object is shared widely by diverse components, it may be impossible to identify a single owner or even a sequence of owners. An object that defies simple characterization and management of its lifetime is likely to be long-lived.

Solution. Allow arbitrary owners to declare unilateral interest in a dynamic object. The lifetime of the dynamic object continues until all of the owners have relinquished their interest in it. Constraints on the owners and the owned object vary depending on the style of implementation.

Examples. A conventional reference-counted implementation of a string class (see The C++ Programming Language) uses Shared Ownership of the representation object, which in turn is Object As Sole Owner of the underlying array of characters. Usually, the representation class is specifically designed to carry a reference count to support its shared ownership; that is, the implementation of the ownership intrudes into the class of the owned object. Ownership is intentionally restricted to the string objects.

Reference counting "smart pointers" (see Advanced C++ Programming Styles and Idioms, by James Coplien, Addison Wesley, 1992) also use Shared Ownership. As with a reference-counted string class, the mechanism usually intrudes into the owned object and restricts ownership to the smart pointer objects.

The following example demonstrates that the intrusion on the owned object and constraints on the owners are not intrinsically part of the pattern.

Shared ownership information with respect to arbitrary objects is recorded in a global instance of the class Shared. Shared::adopt() records an arbitrary owner declaring an interest in an arbitrary object. Shared::disown() records an owner relinquishing that interest. A true result, from disown(), informs the departing owner that no other owners remain, and the dynamic object should be deleted. Effectively, Shared provides reference counting that constrains neither owner nor owned object; see Listing Eight.

A Journal object logs messages to a Window. The current Window is established in its constructor, but may be changed by the bind member function. A Journal object assumes shared ownership of its Window by registering its ownership with clerk and deleting any unowned Window. The Window class does not participate in the implementation of this ownership mechanism. The other owners of Window may be arbitrary functions, objects, and classes. Journal requires only that they observe the protocol with respect to clerk; see Listing Nine.

Dangling Pointers. Any discussion of garbage avoidance often leads to the separate, but related, topic of dangling pointers. A pointer dangles when use of its value becomes invalid at the end of the lifetime of the object that it addresses. Use of a dangling pointer is undefined behavior, which can result in a program crash or worse. A pointer that refers to a dynamic object dangles if the dynamic object is deleted (by its owner). However, dangling pointers can also arise without the use of dynamic allocation: A pointer to an automatic object dangles if the pointer outlives the object. A dangling pointer must never be dereferenced.

Reference Counting versus Garbage Collection. Reference counting (mentioned with respect to Shared Ownership) is easily confused with garbage collection. However, the two mechanisms are not equivalent. There are circumstances under which reference counting reclaims memory that garbage collection misses, and vice versa. By tolerating a dangling pointer, reference counting that depends on an explicit decrement operation can recover a dynamic object that garbage collection must retain. On the other hand, a cycle of mutual references can cause reference counting to overlook a set of objects that garbage collection would reclaim.

Exception Handling. An exception handling (EH) mechanism for C++ (using termination semantics) has been proposed in the draft standard, and implementations are becoming available. Clearly, a nonlocal goto mechanism introduces further analysis in determining that a program is coded correctly, including any use of dynamic memory. Unfortunately, to date, the C++ community has little experience using EH. It appears that object state and resource management (including dynamic memory) are the two key problems to address when using EH and that resource management is the easier of the two.

My thanks to Paul Jensen, David Leserman, Doug Schmidt, John Vlissides, and the members of my PLoP '95 workshop for their comments.

void some::SoleOwner() { . . . Resource *res = new Resource; . . . delete res; . . . }

void Transport::xmit(const Packet *p) { static char *buffer = 0; static int allocated = 0; int sz = formattedSize(p); if( sz > allocated ){ delete [] buffer; buffer = new char[sz]; allocated = sz; } . . . }

class Server { public: Server(); ~Server(); . . . private: class Auxiliary *aux; }; Server::Server() { aux = new Auxiliary; } Server::~Server() { delete aux; }

class Connection { public: Connection(); ~Connection(); void clearErrors(); . . . private: Window *errWin; void error(Text); . . . }; Connection::Connection() { errWin = 0; } Connection::~Connection() { clearErrors(); } void Connection::error(Text t) { if( errWin == 0 ) errWin = new Window; . . . errWin->write(t); } void Connection::clearErrors() { delete errWin; // may be null errWin = 0; }

class Query { public: Query(); ~Query(); . . . private: static DataBase *db; static int population; . . . }; DataBase *Query::db = 0; int Query::population = 0; Query::Query() { if( population == 0 ) db = new DataBase; population += 1; } Query::~Query() { population -= 1; if( population == 0 ){ delete db; db = 0; } }

Resource *Creator::factory() { Resource *res = new Resource; if( res->valid() ) return res; // transfer // retain delete res; return 0; } void Client::usesFactory() { Resource *res = Creator::factory(); if( res != 0 ){ // transferred . . . delete res; // eventually } }

void Use_auto_ptr() { Resource *res = new Resource; auto_ptr<Resource> owner(res); . . . // use res . . . } // owner deletes

template<class R> class Shared { public: void adopt(R*); bool disown(R*); private: map7<void*,int> owners; }; template<class R> void Shared<R>::adopt(R *r) { if( owners.find(r) == owners.end() ) owners[r] = 1; else owners[r] += 1; } template<class R> bool Shared<R>::disown(R *r) { assert( owners.find(r) != owners.end() ); owners[r] -= 1; if( owners[r] > 0 ) return false; owners.erase(r); return true; }

class Journal { public: void bind(Window*); void log(Message); Journal(Window*); ~Journal(); . . . private: Window *current; . . . }; void Journal::bind(Window *w) { clerk.adopt(w); if( clerk.disown(current) ) delete current; current = w; } void Journal::log(Message m) { current->display(render(m)); } Journal::Journal(Window *initial) { current = initial; clerk.adopt(current); } Journal::~Journal() { if( clerk.disown(current) ) delete current; }