Dr. Dobb's Journal March 1999
This month puts Quincy 99 and D-Flat 2000 on hold while I continue my discussion of the migration from C++ to Standard C++. But first, the book of the month.
C++ programmers should have a copy of the Standard C++ specification. When you question how a code idiom should work, the document is the final authority because it is supposed to tell compiler implementors what to do. That isn't to say that the document is necessarily an accurate reference of the C++ language that your compiler implemented; they're all still playing catch-up. However, if you code to the document and your compiler accepts your code, you can be assured that your code is reasonably correct. If your code looks correct and the compiler rejects it or compiles something other than what the document says, chances are the compiler hasn't achieved full compliance yet. Few if any C++ books on the market today fully and accurately cover Standard C++. Most use code idioms that can be compiled rather than ones that reflect the standard. Until all parties -- book authors and compiler implementors -- get in step, the standard document is the only trustworthy guide to writing standard C++.
You can purchase a copy of the standard in PDF format, which you can view by using the ubiquitous and free Adobe Acrobat reader (http://www.adobe.com/). You can download the standard document from http://webstore.ansi.org/ansidocstore/default.asp/. You'll need to provide a credit card number. The document costs $18.00 and is well worth the money. It's almost as good as an HTML version, which is not available. The main deficiency is that the document fails to provide internal links when passages refer to other passages, apparently reflecting a deficiency in the Acrobat PDF document model.
Last September and October I discussed several C++ language features and behaviors that the C++ standardization effort added to the language. Included were discussions of the bool and wchar_t types, the scope of a variable declared in the first controlling expression of a for statement, similar new behavior in if, while, and switch expressions, new behavior for enum, overloaded new[] and delete[], placement new and delete, new style headers, and the namespace feature. That discussion continues this month with some observations about two language features that the ANSI/ISO committee did not invent but that were significantly changed as a result of the committee's deliberations.
The Annotated C++ Reference Manual (the "ARM") by Margaret Ellis and Bjarne Stroustrup (Addison-Wesley, 1990) was the first formal definition of templates and exception handling, two C++ language features that the authors described as "experimental." Coincident with that publication, some commercial compilers implemented the new features to comply as closely as they could with the ARM's definition, modifying those implementations as the committee made changes and converged on an approved standard and as the features became better understood. Consequently, in the eight years since the ARM was published, programmers have gained much experience in the use of templates and exception handling, but a few of the committee's additions have raised some debate among the C++ programming community, particularly with respect to committee additions to exception handling. Some programmers question the effectiveness of the changes and others defend them. I do not intend to add to the debate, but I will address some of the issues, explain how they work, and consider their implications from the perspective of a programmer who uses them. First, however, I will discuss my own experience with the partial specialization of templates.
Template specialization allows you to provide an implementation of a template for a particular type. The first class template in Listing One parameterizes types called T and A. The second class template is a specialized version that the compiler uses whenever the program instantiates an object of type vector for parameterized types of int and char*.
The ARM does not address template specialization, although compilers have implemented the feature for several years, so we can regard it as a reasonably well-understood feature that was introduced by the committee -- that is, not a feature of traditional ARM C++.
Don't be confused by my use of the identifier vector in Listing One. It is not the same as std::vector because it is not in the std namespace. My reason for using this particular name becomes apparent soon. Incidentally, template specialization is the only place I have found where the standard permits a programmer to add declarations and definitions to the std namespace. Your compiler might allow it, but the practice is considered a bad one and is forbidden usage by the language of the standard specification.
Partial specialization lets you provide a specialized version of a template wherein the specialization specializes some, but not all, of the arguments. Listing Two is a partially specialized version of the vector class template. You can similarly specialize a function template.
In The C++ Programming Language, Third Edition, (Addison-Wesley, 1997), Bjarne Stroustrup uses inheritance to demonstrate partial specialization by using an idiom similar to Listing Three. His example demonstrates how to partially specialize a class for void pointer arguments and then inherit from that class for specializations of parameterized types. It is the only example of partial specialization in the book; I do not think it is a particularly representative one because by using inheritance, the example goes beyond the idiom of partial specialization to demonstrate partial specialization. Observe that in Listings Two and Three, the partially specialized class templates inherit nothing from the class templates that they specialize. You must fully reimplement all the class template's behavior.
Suppose, however, that you want to specialize the behavior of a member function of a somewhat complex class template. You'd rather not reimplement the entire class template. You can fully specialize a class template member function as Listing Four shows. Class template member function specializations are supposed to be treated the same as function template specializations, but the compiler implementation that I use (egcs 1.1) does not permit partial specialization of class template member functions as Listing Five tries to do. I cannot find a specific reference to or example of this kind of specialization of member functions in the standard document. However, Dr. Stroustrup thinks that full and partial specialization of class template member functions is permitted. I guess I'll have to wait for the compilers to comply.
So why do I want to partially specialize class template member functions? One year ago in this column I published the Persistent Template Library (PTL), which uses inheritance to specialize the behavior of the Standard C++ container classes so that they are persistent; the instantiation of a persistent container object invokes its constructor, which fills the container with the persistent values from the object store disk file. If users modify the container in memory, the destructor rewrites the container contents to the disk file. I wanted to implement persistence with an allocator that the user could specify to override the default allocator argument for the various container classes. This approach was not possible given the state of the PC compilers this time last year ("this time," of course, included publishing lead time). Standard C++ does not provide a mechanism with which the allocator can tell the container that it is allocating and filling memory with data values. Allocators do not know anything about the containers that use them. Containers allocate the memory they need assuming that they are empty when constructed and that the using program will fill them with data (except, of course, when they are constructed from other containers). I said last year:
I thought I might make the persistent allocator concept work by partially specializing each container to overload its constructors to communicate with the allocator. Until PC compilers implement partial specialization, however, I cannot experiment with this approach.
Discussions in comp.std.c++ reminded me of this approach, and one participant, Nathan Myers, a frequent contributor to DDJ, even suggested that I use it now that partial specialization is widely implemented. My first attempt to do so involved the partial specialization of the std::vector constructor for instantiations that override the default allocator with a custom persistent allocator to see if I could make the allocator fill the container with values during construction. As it turned out, I could not take that approach given the state of compilers today (at least in this shop) because, as I pointed out earlier, apparently one cannot partially specialize a member function. I would have to partially specialize all of std::vector and the other container classes, instead, and reimplement all their behavior, which I do not want to do. Someone has already done that work. I prefer to use existing efforts rather than duplicate them.
There has been some controversy about the new C++ exception specification feature. Some programmers feel that the feature should not have been included in the language, and, given that it has, should not be used.
An exception specification adds code to a function header to specify what exceptions may be thrown after the function is called and before the function returns. This code tells the programmer who calls a function, which exceptions the calling function should be prepared to catch, and alerts as to the consequences of not catching the potential exceptions. Listing Six is a function header with an exception specification.
The function in Listing Six is permitted to throw only exceptions of type MyException and types publicly and unambiguously derived from MyException. To specify more than one exception, use a comma-separated list. The compiler is not required to issue a diagnostic if the program violates this permission; after all, the compiler cannot know about all the exceptions that might be thrown by other functions that are directly or indirectly called by the current function.
If a function has no exception specification, the function is permitted to throw any exception. If a function has an empty exception specification as Listing Seven shows, the function may not throw any exceptions.
These choices were intentional. A more intuitive interface would disable exceptions when no specification is given and use something like throw (...) to specify that any exception may be thrown. These proposals are not workable because of the effect on existing code that predates the exception specification feature. In The C++ Programming Language, Third Edition, Stroustrup says:
...that would require exception specifications for essentially every function, would be a significant cause for recompilation, and would inhibit cooperation with software written in other languages.
If the program violates the exception specification, the system calls the special unexpected() function, which you may override to throw an acceptable exception. If unexpected() throws an unacceptable exception (or, I expect, tries to return -- the standard does not specify what happens if unexpected() tries to return; it only says that it must not), the system calls terminate(), unless the violated exception specification includes std::bad_ exception, whereupon the system converts whatever unexpected() throws to std::bad_ exception. Got that? It ain't intuitive.
So, what was the controversy all about? Some programmers believe that exception specifications are good only for documentation when you get them right and can constitute a debugging hazard when you get them wrong and unexpected terminations result. When a function has an exception specification, an unexpected thrown exception is not caught by the function's catch(...) handler if it has one. Presumably, a programmer knows what exceptions are likely to be thrown and why. If you do not know of all the potential exceptions, there's not much you can do in a catch(...) handler other than intercept the catch and use your debugger's call stack to see where it came from. If you let the system call terminate(), it's too late to see where it happened, unless you override terminate(). It might be anywhere in those thousands of lines of code.
Questions also arise about how to properly use exception specifications in function templates and class template member functions when the very "genericity" of the template concept removes all knowledge of the types being parameterized from the author of the functions. Furthermore, because exception specification compliance is typically checked at run time rather than at compile time, there is no static check that a particular template instantiation is likely to cause a call to terminate() following an unaccepted throw. It would seem that templates and exception specifications are not comfortable with one another, at least in their current implementations.
Well, it isn't 2000 yet. Nonetheless, some readers have written to ask about D-Flat 2000, my C++ framework based on free software development tools. When will I finish it? When will I publish it? The answer is that I need to complete the Quincy 99 compiler suite first. The work on the IDE is mostly finished (http:// www .midifitz.com/alstevens/quincy99) with only the integration of the programmer's editor (http://www.midifitz .com/alstevens /editor) remaining. I now eagerly await the availability of a Standard C++ library for the egcs 1.1 compiler, which Quincy 99 uses. I await this development because I want the framework to be based on current technology. Such a library is under development, and if you want to participate, you can do so by visiting http://sourceware .cygnus.com/libstdc++/. I'm writing this column toward the end of December, 1998, and the experimental library still lacks several necessary components. Perhaps by the time you read these words, the library will be close to completion.
DDJ
// -- - fully parameterized vectortemplate <class T, class A>
class vector {
public:
vector(T t, A a) { }
};
// -- - fully specialized vector
template <>
class vector<int, const char*> {
public:
vector(int t, const char* cp) { }
};
int main()
{
vector<int, int> fc(1,2); // uses parameterized vector
vector<int, char*> fi(1,"1"); // uses specialized vector
}
// -- - partially specialized vectortemplate <class T>
class vector<T, char> {
public:
vector(T t, char) { }
};
int main()
{
vector<int, char> fp(1,'a'); // uses partially specialized vector
}
template<class T, class A>class vector {
public:
vector(T, A) {}
};
template<class T> class vector<T, void*> {
public:
vector(T, void*) {}
};
template<class T, class A> class vector<T, A*> : private vector<T, void*> {
public:
vector(T t, A* a) : vector<T, void*>(t, a) { }
};
int main()
{
int i = 123;
char* cp = "filename";
vector<int, char*> vip(234, cp);
}
template <class T, class A>class vector {
public:
vector(T t, A a) { }
void f() { }
};
// -- - specialized f()
template <> void vector<int,int>::f()
{
}
int main()
{
vector<int, int> fc(1,2); // uses regular f()
fc.f();
vector<int, char*> fi(1,"1"); // uses specialized f()
fi.f();
}
// -- - partially specialized f()template <class T> void vector<T, int>::f()
{
}
void f() throw(MyException){
// ...
}
void f() throw(){
// ...
}