Once you start replacing macros with templates, it's hard to know when to stop.
Copyright © 1998 Robert H. Schmidt
This month I finally end our exploration of auto_pointer, solve a reader's problem, bring happy news from the C++ front, issue a couple of corrections, and tack on the usual stray thought.End of the Road
As a coda to our auto_pointer opus, I want to sketch a few more variations you can play, based on counterpoint from Diligent Reader Marcus Barnes.
In my September 1997 column, a macro encapsulated behavior common to all classes derived from the auto_pointer abstract base. In that scheme, the only member that differed from one derived class to another was the destructor; everything else was implemented by the macro.
Marcus suffered indigestion at this (ab)use of a macro, and proposed a template-based solution. Since templates are glorified macros anyway, Marcus's underlying design is not all that different from mine. In execution his idea is simple: pass a second argument to the auto_pointer template, designating the deallocation scheme for the pointed-to object:
template<class T, class Allocator> class auto_pointer { public: ~auto_pointer() { if (is_owner_ && pointer_ != NULL) Allocator::deallocate(pointer_); } // ... other members same as // before };Previously we derived multiple objects from auto_pointer, each with a common implementation encapsulated in a macro; now we have one auto_pointer class, and pass it one of multiple Allocators:
template<class T> class free_store_allocator { public: static void deallocate(T * const pointer) { free(pointer); } }; template<class T> class heap_allocator { public: static void deallocate( T * const pointer) { delete pointer; } }; // // destructor calls 'free' // on contained pointer // auto_pointer< int, free_store_allocator<int> > fp = (int *) malloc(sizeof(int)); // // destructor calls 'delete' // on container pointer // auto_pointer< char, heap_allocator<char> > hp = new char;I originally used macros in a similar class hierarchy a couple years ago to get around a particular compiler's lack of proper template support. I continued to use them in my column, largely to avoid problems you might have with similarly crippled compilers, and to keep my examples' template-speak to a minimum.
In hindsight, I think I should have used Marcus's method all along. It is certainly more aesthetic, and better leverages C++'s design strengths. In fact, I'm such a convert that I'm going to break the promise I made last month, and not develop the macro method any farther. Instead I'll show all remaining modifications using this new template design.
Alternatives
Variations on this template technique abound. Perhaps most obvious is extending it to other kinds of deallocation schemes. Possibilities include an auto_pointer containing a Microsoft COM object:
template<class T> class COM_allocator { public: static void deallocate(T * const pointer) { // assume pointer was // AddRef'ed pointer->Release(); } }; auto_pointer< int, COM_allocator<int> > p; some_COM_call(&p);and one requiring no deallocation at all [1]:
template<class T> class null_allocator { public: static void deallocate(T * const) { } }; int array[10]; auto_pointer< int, null_allocator<int> > p = array;You could instrument auto_pointer to "NULL" out a pointer once it's deallocated, as a debugging aid and to help prevent dangling pointers:
template<class T> class heap_allocator { public: static void deallocate(T *&pointer) // changed { delete pointer; pointer = NULL; // new } };And of course, in addition to automatically deallocating pointers, you could also automatically allocate them:
template<class T> class heap_allocator { public: static T *allocate(T const &value) // new { return new T(value); } // ... }; template<class T, class Allocator> class auto_pointer { public: auto_pointer(T const &value) { pointer_ = Allocator::allocate(value); // new } // ... }; auto_pointer< int, heap_allocator<int> > p = 123;Streamlining
If you reckon most of your auto_pointers will come from the heap, set that as the default allocation scheme:
template<c lass T, class Allocator=heap_allocator<T> > class auto_pointer { // ... }; auto_pointer<int> p; // same as auto_pointer< int, heap_allocator<int> > p;For the other allocation schemes, you can tidy the syntax (ironically enough) with macros:
// original auto_pointer<int, free_store_allocator<int> >p1; // alternative using macro #define AUTO_POINTER(T, Allocator)\ auto_pointer< T, Allocator<T> > AUTO_POINTER(int, free_store_allocator) p2;thereby eliminating the redundant int argument.
If you absolutely must have your auto_pointer macro-unsaturated, you can borrow a recipe from C++'s Standard Template Library (STL):
template<class T> class free_store_allocator { public: typedef T value_type; // ... }; template<class Allocator> class auto_pointer { private: typedef typename Allocator::value_type T; // ... }; auto_pointer< free_store_allocator<int> > p;How this works:
Thus auto_pointer indirectly discovers its contained type, without requiring that type as an explicit template argument.
- auto_pointer is passed an allocation type.
- That allocation type exports the symbol value_type, aliased to the type of object allocated.
- auto_pointer in turn aliases an internal symbol T to the allocator's value_type. This is equivalent to the T passed in as a second template argument in the earlier auto_pointer version.
Traits
While I did not design our months-long auto_pointer derivation as an introduction to STL, I find that identifying and solving many of the auto_pointer design issues leads to greater understanding of STL. That library uses traits like value_type in abundance. Such traits are especially helpful for abstracting differences between class and non-class types.
In our example, all auto_pointer objects could export a standard symbol for their contained object type:
template< class T, class Allocator=heap_allocator<T> > class auto_pointer { public: typedef T value_type; // ... };You could then create a container of objects referencing an auto_pointer's underlying value_type:
template<class Pointer> class container { private: typename Pointer::value_type *pea; // ... };While this works when the passed-in Pointer is an auto_pointer:
container< auto_pointer<int> > can_o_peas; // OKit fails if Pointer is a real C++ pointer:
container<int *> can_o_peas; // error, 'int *' has no // member 'value_type''You clearly can't add members such as value_type to real pointers, but you can construct another class that describes the pointer's traits [2]. In the case of auto_pointers, the traits class simply aliases information the auto_pointer class already provides:
template<class Pointer> class pointer_traits { public: typedef Pointer::value_type value_type; // ... };For traits of real pointers, the traits class infers the value type:
template<class T> class pointer_traits<T *> { public: typedef T value_type; // ... };The upshot is that the pointer_traits<x>::value_type abstraction always works, whether x is a class or a real C++ pointer:
template<class Pointer> class container { private: typename pointer_traits<Pointer>::value_type *pea; // ... };Although this example is a bit strained, it does help show how STL internals bolt together.
Next Adventure
Barring some epiphany, this should be my last missive on auto_pointer and its kin. If your appetite for souped-up pointers is insatiable, I invite your perusal of Chapter 24 within the C++ Standard, which describes STL iterators in all their glory. Pay particular attention to iterator taxonomy (the tables in section 24.1) for ideas on your own auto_pointer derivatives.
Next month we'll start designing an abstracted array-like class. Just as auto_pointer lies somewhere on the evolutionary scale between C++ pointer and STL iterator, I expect the array class to fall between C++ array and STL container.
Well, almost. I find the C++ array property set impossible to fully emulate; our class's behavior will therefore not be a strict superset of an array's, although it should come close.
Today's Scoreboard Stumper
Self-proclaimed "avid reader" Bill Palladino ran into some problem code, which I've reworked as
class B1 { // ... }; class B { public: static B1 b1; }; class A1 { public: A1() { B::b1; } }; class A { public: static A1 a1; };Here's Bill's dilemma:
- Class A has a static member a1 defined in a.cpp.
- Class B has a static member b1 defined in b.cpp.
- Object A::a1's constructor directly or indirectly references object B::b1.
- Therefore B::b1 must always construct before A::a1 references it.
- In practice, B::b1 sometimes constructs before A::a1 and sometimes doesn't.
Bill stumbled upon a much-forgotten aspect of both C and C++: the relative initialization order of externally-linked objects. If a.cpp and b.cpp both define objects visible to the linker (and thus to one another), we can't know if a.cpp's objects initialize before or after b.cpp's [3].
Such chaotic ordering exists only across translation units. If Bill defined A::a1 and B::b1 in the same translation unit, they would construct in their order of definition. However, I dislike bundling unrelated definitions in the same translation unit, and wanted to find Bill a different strategy.
And The Final Jeopardy Answer Is
I gave Bill several possible solutions. The one he chose changes B::b1 into a function member that in turn encapsulates a B1 object:
class B1 { // ...same }; class B { public: static B1 &b1() // changed { static B1 b1_; // new return b1_; } }; class A1 { public: A1() { B::b1(); // changed } }; class A { // ...same };This design has no externally linked object B::b1 to initialize, and thus suffers no uncertainty of initialization ordering, to wit:
- Object b1_ (with no linkage and static duration) is constructed the first time its enclosing block (function B::b1) is entered.
- Function B::b1 is first entered in the A1 constructor.
- The A1 constructor is first called when object A::a1 is created.
Net result: object b1_ constructs just before object A::a1 references it.
When I told Bill I'd be answering his question in print, he wrote a tender reply expressing his infinite gratitude for such an honor:
"My name in the CUJ?! I can't believe it at long last I'd get my 15-microseconds of fame."
He also wondered
"Can I get my picture on the cover?"
As if! Even we CUJ editors can't get our pictures published although come to think of it, that may be to everyone's advantage. I certainly am not pining for the Orwellian countenance of my Editor-in-Chief.
By the way, if you're new to C++, you may have been surprised to learn that the original a1 and b1 were conceptually extern, especially since their declarations featured the keyword static. That keyword has conflicting meanings depending on context, and is an unfortunate choice; a keyword like noninstanced or per_class would be less ambiguous.
The C++ Committee has not been shy about adding new keywords. I have to believe that were it not for legacy C++ code, Committee members would have created a new keyword here.
Pass Out the Cigars
But we have come to praise the Committee, not to bury it. After an eight-year gestation, it has finally given birth to the Final Draft Information Standard, or FDIS [4]. For most practical concerns, you should consider the Standard a done deal. Technically, the ISO bureaucracy still needs to check for dotted i's and crossed t's; they then hand the Draft over to their Electoral College for a final formal ballot.
By the time you read this, the Standard should be publicly available. For more information, see Pete Becker's column in this month's issue.
Erratica
Two issues back (December 1997) I discussed a letter from Diligent Reader David R. Tribble. Little did I know that he was also Diligent Writer David R. Tribble, for he has a feature article in the same CUJ issue. Such a small circle we travel in next thing you know, I'll start getting fan mail from Dan Saks [5].
Last month I mentioned a couple of features missing from Metrowerks' Mac OS compiler. One week after I submitted that article, Metrowerks sent me their latest release, CodeWarrior Pro version 2. I am happy to report that CodeWarrior finally supports namespaces in general, and namespace std in particular. It also now recognizes default template arguments and some template specialization. (Unfortunately, however, it has yet to understand all the specialization required by STL).
Other improvements in version 2 include wchar_t as a keyword, and const static data member initialization. For details on these changes and more, check out the Metrowerks home page at <http://www.metrowerks.com>.
And Finally...
It's my turn to ask a personal favor of you. Having written my first programs on them, I harbor a fondness for Texas Instruments programmable calculators from the mid/late 70's. I am especially interested in the TI-59 and the enigmatic TI-88. If you have any information, material, or merchandise relating to these machines, and would like to find a loving home for same, please drop me a line. Thanks. o
Notes
1. See last month's column, where I present an auto_pointer-like class that does not automatically release its contained pointer.
2. Caveat: this technique requires partial template specialization, which some compilers do not yet support.
3. For a full treatise on linkage specification, see Dan Saks' column in the November 1997 CUJ.
4. U.S. readers, please don't confuse this with the FDIC. The financial security of your C++ code is most assuredly not guaranteed by FDIS.
5. At least he knows who I am. I think he's still figuring out that SK Boothill guy, or whatever his name is.
Bobby Schmidt is a freelance writer, teacher, consultant, and programmer. He is also a member of the ANSI/ISO C standards committee, an alumnus of Microsoft, and an original "associate" of (Dan) Saks & Associates. In other career incarnations, Bobby has been a pool hall operator, radio DJ, private investigator, and astronomer. You may summon him at 14518 104th Ave NE Bothell WA 98011; by phone at +1-425-488-7696, or via Internet e-mail as rschmidt@netcom.com.