Robert Schmidt
May 10, 1999
In his inaugural column, Robert Schmidt shows you how to handle exceptions in C++.
G'day all. Bobby Schmidt here, the newest MSDN Online Voice, welcoming you to Deep C++, my regular column for programmers using C and C++. As part of my introduction, I want to give you some background on who I am and how this column fits within the MSDN pantheon.
If getting paid makes one a professional, then my programming career started eighteen years ago in my hometown of Dayton, Ohio. Longing for more interesting geography and less interesting weather, I escaped to Seattle in 1987. Two years later I started working for Microsoft. Eventually chafing under the yoke of company ownership, I turned freelance in 1995. (Perhaps ironically, Microsoft continues to be an important client.) Along the way I've lived in Sydney, Australia, the teeny upstate New York town of Limestone, and gorgeous Sedona, Arizona.
As this column's name suggests, I'll be examining the C and C++ languages themselves, rather than a specific technology. In my experience programmers become too easily fixated on a particular "cool" technology, forgetting it is a programming language that brings that technology into being. Does this put me at odds with the traditional MSDN mission? I don't think so.
Mastery of an expressive medium translates directly into one's quality of expression through that medium. I've found, however, that too many programmers hold incomplete or erroneous notions of how their media work, leading to porous designs and accidental implementations. For that reason I would like to focus attention on the languages—the media—themselves.
I am technology-agnostic by both nature and profession (although I must admit a fondness for Macs, as they are the only computers I use at home). But everyone cares about certain technologies more than others; given that you are reading me on MSDN, you're probably concerned with Microsoft technologies in particular. Yet all the Win32® wisdom in the universe won't help if your underlying language use is incorrect. C and C++ can be tremendously powerful allies but, as with all things powerful, today's mismanaged ally can become tomorrow's enemy.
I aim to help you understand the languages from the inside out, and then apply that knowledge to specific technology domains. To that end I will frequently allude to the ISO Standards that define the language rules. I will also demystify wizards and wrappers, showing how and why the underlying code works as it does. If a particular technology relies on interesting, non-obvious, or non-standard language features, I'll uncloak that too. And I'll take every opportunity to help you write more robust, maintainable, and portable programs. Call me a heretic, but I don't believe you'll program only Microsoft technology your entire life.
All Deep C++ columns will assume you have at least an intermediate knowledge of (and comfort with) C. The C++ topics assume similar acumen with that language. Where practical I will demarcate material applying to both languages from that applying to C++ alone. (Unless I discuss C9X, the forthcoming version 2.0 of Standard C, probably no material will be specific to C.)
On the professional advice of Dr. GUI, I'm devoting my first column series to program exceptions. Now, I realize that the term "exception" is somewhat ambiguous and context-sensitive, especially in light of C++ Standard exceptions and Microsoft's Structured Exception Handling. Unfortunately, the notion of exceptions is enshrined in both the language Standards, as well as in the common programming literature. As I'm loath to invent a new term, I'll do my best to make my usage clear in each series part:
(You C denizens might be tempted to bail after Part 2, but I encourage you to stick it out; many of the ideas I'll present will still apply to C, albeit somewhat indirectly.)
At their core, program exceptions are conditions that arise infrequently and unexpectedly, generally betray a program error, and require a considered programmatic response. Failure to obey that response often renders the affected program crippled or dead, sometimes taking the entire system down with it. Unfortunately, crafting robust code using traditional defensive techniques often trades one problem (crashing exceptions) for another (more tangled design and code).
Too many programmers, deciding the tradeoff isn't worth the bother, have elected to live dangerously. Recognizing this, those crafting the C++ Standard added an elegant and largely invisible exception mechanism to the language—or so the theory goes. As we'll see starting in Part 4, that theory succeeds in the large, but can fail in subtle ways.
In this series I'll show you different C and C++ approaches to managing exceptions, tying each approach to the stages in an exception's lifetime:
In the pathological case of a terminating exception handled outside the program (by the run-time library or operating system), recovery often isn't possible, and the program aborts.
I will purposely ignore hardware error events, as they are properly the domain of low-level platform-specific code. I will instead assume that some software-detectable error has occurred, giving rise to a software exception object in Stage 1.
The Standard C Library provides several mechanisms for exception management. All are available in Standard C++ as well, although the relevant header names have changed: The old C Standard headers with names of the form <name.h> have been mapped to the new C++ Standard headers <cname>. (The header-name prefix c is a mnemonic, suggesting these are all C Library headers.)
Though the old C headers have been retained in C++ for backward compatibility, I recommend that you use the new headers wherever possible. For most practical purposes, the biggest change is that declarations in the new headers appear within namespace std. As an example, the C usage
#include <stdio.h>
FILE *f = fopen("blarney.txt", "r");
is replaced in Standard C++ by
#include <cstdio>
std::FILE *f = std::fopen("blarney.txt", "r");
or the more C-esque
#include <cstdio>
using namespace std;
FILE *f = fopen("blarney.txt", "r");
Unfortunately, Microsoft's Visual C++® does not declare the contents of these new headers in namespace std, even though this behavior is required by the C++ Standard (subclause D.5). Until Visual C++ supports std correctly in these headers, I'll use the old C-style names in my columns.
(In fairness to library vendors like Microsoft, implementing these C Library headers correctly quite possibly requires the maintenance and testing of two distinct code bases—hardly an inviting task, and likely not worth the incremental gain.)
Next to outright ignoring an exception, probably the easiest response is self-destruction. In some cases, that lazy response is actually the correct response.
Before you scoff, consider that some exceptions betray a condition so severe that reasonable recovery is unlikely anyway. Perhaps the best example is malloc of a small object returning NULL. If the free store manager can't scrape together a few spare contiguous bytes, your program's robustness is severely compromised, and the odds of elegant recovery are slim.
The C Library header <stdlib.h> provides two program-halting functions: abort and exit. These functions implement exception lifetime Stages 4 and 5. Neither returns to its caller, and each causes the program to end. As such they represent the ultimate in terminating exception handlers.
While the two are related in concept, they have different effects:
You generally call abort in response to a catastrophic program failure. Because abort's default behavior is immediate program termination, you are responsible for saving your vital data before making the abort call. (As we'll see in the discussion of <signal.h>, you can persuade abort to call clean up code automatically.)
By contrast, exit performs custom clean up via the registered atexit handlers. These handlers are called in the reverse order of their registration. You can think of them as pseudo-destructors. By putting critical clean-up code in these handlers, you ensure program termination leaves no loose ends. Example:
#include <stdio.h>
#include <stdlib.h>
static void atexit_handler_1(void)
{
printf("within 'atexit_handler_1'\n");
}
static void atexit_handler_2(void)
{
printf("within 'atexit_handler_2'\n");
}
int main(void)
{
atexit(atexit_handler_1);
atexit(atexit_handler_2);
exit(EXIT_SUCCESS);
printf("this line should never appear\n");
return 0;
}
/* When run yields
within 'atexit_handler_2'
within 'atexit_handler_1'
and returns a success code to calling environment.
*/
(Note that if your program returns from main without explicitly calling exit, your registered atexit handlers are called anyway.)
Neither abort nor exit returns to its caller, and each causes the program to end. In that sense they represent the ultimate in terminating exceptions.
abort and exit let you terminate your program unconditionally. You can also terminate your program conditionally. The supporting mechanism is every programmer's favorite diagnostic tool: the assert macro defined in <assert.h>. This macro is typically implemented as shown:
#if defined NDEBUG
#define assert(condition) ((void) 0)
#else
#define assert(condition) \
_assert((condition), #condition, __FILE__, __LINE__)
#endif
As this definition shows, assertions are inactive when the macro NDEBUG is defined, implying assertions are meant only for debug versions of your code. As a result, the asserted condition is never evaluated in non-debug code. This can lead to surprising differences between debug and non-debug versions of the same program:
/* debug version */
#undef NDEBUG
#include <assert.h>
#include <stdio.h>
int main(void)
{
int i = 0;
assert(++i != 0);
printf("i is %d\n", i);
return 0;
}
/* When run yields
i is 1
*/
Now change the code version from debug to release, by defining NDEBUG:
/* release version */
#defing NDEBUG
#include <assert.h>
#include <stdio.h>
int main(void)
{
int i = 0;
assert(++i != 0);
printf("i is %d\n", i);
return 0;
}
/* When run yields
i is 0
*/
To avoid these differences, ensure the expressions assert evaluates don't include meaningful side effects.
In the debug-only definition, assert becomes a call to the function _assert. I made that name up—your Library implementation can reference any internal function it wants. Whatever it's called, that function generally has the following form:
void _assert(int test, char const *test_image,
char const *file, int line)
{
if (!test)
{
printf("Assertion failed: %s, file %s, line %d\n",
test_image, file, line);
abort();
}
}
Thus, a failed assertion issues a diagnostic detailing the failed condition, plus the offending source file name and line number, before calling abort. The diagnostic mechanism I show here (printf) is fairly primitive; your Library implementation may generate feedback through a more sophisticated means.
Assertions implement exception Stages 3 through 5. They are really an annotated abort with a pre-condition check; if the check fails, the program halts. You typically use assertions to debug logic errors, conditions that can never arise in a correct program:
/* 'f' never called by other programs */
static void f(int *p)
{
assert(p != NULL);
/* ... */
}
I contrast logic errors with other run-time errors that can occur in a correct program:
/* ...get file 'name' from user... */
FILE *file = fopen(name, mode);
assert(file != NULL); /* questionable use */
Such errors represent exceptional conditions, but aren't bugs. For these run-time exceptions, assert is probably an inappropriate response; you should instead favor one of the other mechanisms I present next.
Compared to the drastic abort and exit, the goto statement might seem like a more scalable method for managing exceptions. Unfortunately gotos are local: They can jump to a label only within their enclosing function, and thus cannot transfer control to any arbitrary point in a program (unless, of course, you stuff all your code into main).
To get around this restriction, the C Library provides functions setjmp and longjmp, which act as non-local labels and gotos respectively. The header <setjmp.h> declares these functions, along with the collateral type jmp_buf. The mechanics are fairly simple:
By having two classes of return values, setjmp lets you determine how it's being used. When setting j, setjmp works as you normally expect; but as the target of a long jump, setjmp "wakes up" from outside its normal context.
You use longjmp to raise a terminating exception, and setjmp to mark the corresponding exception handler:
#include <setjmp.h>
#include <stdio.h>
jmp_buf j;
void raise_exception(void)
{
printf("exception raised\n");
longjmp(j, 1); /* jump to exception handler */
printf("this line should never appear\n");
}
int main(void)
{
if (setjmp(j) == 0)
{
printf("'setjmp' is initializing 'j'\n");
raise_exception();
printf("this line should never appear\n");
}
else
{
printf("'setjmp' was just jumped into\n");
/* this code is the exception handler */
}
return 0;
}
/* When run yields:
'setjmp' is initializing 'j'
exception raised
'setjmp' was just jumped into
*/
The function filling the jmp_buf must not return before you call longjmp. Otherwise the context restored from the jmp_buf will be invalid:
jmp_buf j;
void f(void)
{
setjmp(j);
}
int main(void)
{
f();
longjmp(j, 1); /* logic error */
return 0;
}
You must therefore treat setjmp as a non-local goto within the current calling context only.
In combination, longjmp and setjmp implement exception lifetime Stages 2 and 3. longjmp(j, r) creates the exception object r (a single integer), and then propagates that object as the return value from setjmp(j). The setjmp call, in effect, is notified of the exception r.
The C Library also has a unified (albeit primitive) event-management package. This package defines a set of events or signals, along with Standard methods to raise and handle them. These signals betray either an exceptional condition or an asynchronous external event; for the purposes of this discussion, I'll focus on exception signals only.
To use the package, include the Standard header <signal.h>. This header declares the functions raise and signal, the type sig_atomic_t, and signal event macros with names starting SIG. The Standards mandate six signal macros, although your Library implementation may add others. The set of signals is fixed to those defined in <signal.h>—you cannot extend the set with your own custom signals. Signals are generated by calls to raise and captured in handlers. The run-time system provides default handlers, but you can install your own via signal. Handlers can communicate to the outside world through objects of type sig_atomic_t; as the type name suggests, assignment to such objects is atomic, or interrupt-safe.
When you register a signal handler, you normally provide the address of a handler function. Such functions must accept an int value (the signal event being handled), and return void. In this way, signal handlers are like setjmp; the only exception context they receive is a single integer:
void handler(int signal_value);
void f(void)
{
signal(SIGFPE, handler); /* register handler */
/* ... */
raise(SIGFPE); /* invoke handler, passing it 'SIGFPE' */
}
Alternatively, you can install two special handlers:
In all cases, signal returns a pointer to the previous handler (implying registration success), or SIG_ERR (implying registration failure).
Handlers are called, implying signals are resuming exceptions. However, you are free to call abort, exit, or longjmp from an exception handler, effectively interpreting the signal as a terminating exception. In an interesting twist, abort itself actually calls raise(SIGABRT) internally. The default SIGABRT handler issues a diagnostic and terminates the program, but you can install your own SIGABRT handler to change this behavior.
What you can't change is abort's termination of the program. abort is conceptually written as shown:
void abort(void)
{
raise(SIGABRT);
exit(EXIT_FAILURE);
}
That is, even if your SIGABRT handler returns, abort halts your program anyway.
The C Standard imposes other restrictions and interpretations on signal handler behavior. If you have access to the C Standard, I invite you to check out subclause 7.7.1.1 for details. (Unfortunately, neither the C Standard nor the C++ Standard is available on the Internet.)
The <signal.h> declarations cover all stages of an exception's lifetime, cradle to grave. Within the Standard C run-time library, they are the closest thing to a complete exception-management solution.
The <setjmp.h> and <signal.h> routines use the notification style of detecting exceptions: a handler awakens when notified of an exception event. If you'd rather use the polling method, the Standard Library provides an example via the header <errno.h>. This header defines errno plus several values errno may take on. The Standards mandate three such values—EDOM, ERANGE, and EILSEQ for domain, range, and multibyte-sequence errors—but your compiler may add others, all starting with E.
errno, combined with both the Library code that sets it and the user code that interrogates it, implements Stages 1 through 3 of an exception's lifetime: The Library generates the exception object (a single integer), copies the exception object's value to errno, and then relies on user code to poll for and detect the exception.
The Library uses errno principally within its <math.h> and <stdio.h> functions. errno is set to 0 at program start; after that, no Library routine sets errno to 0 again. Thus, to detect an error, you must set errno to 0, call a Library routine, and then check errno's value:
#include <errno.h>
#include <math.h>
#include <stdio.h>
int main(void)
{
double x, y, result;
/* ... somehow set 'x' and 'y' ... */
errno = 0;
result = pow(x, y);
if (errno == EDOM)
printf("domain error on x/y pair\n");
else if (errno == ERANGE)
printf("range error on result\n");
else
printf("x to the y = %d\n", (int) result);
return 0;
}
Note that errno doesn't necessarily reference an object:
int *_errno_function()
{
static int real_errno = 0;
return &real_errno;
}
#define errno (*_errno_function())
int main(void)
{
errno = 0;
/* ... */
if (errno == EDOM)
/* ... */
}
You can adopt this same technique to your own routines, crafting analogues of errno and its values. Using C++, you can, of course, extend the strategy to objects and functions within classes or namespaces. (In fact, among C++ cognoscenti, this technique is the basis for the so-called Singleton Pattern.)
errno-like exception objects are not without limitations:
In sum, such objects are fragile: You can too easily misuse them, with no warning from your compiler and possibly no indicative behavior from your program.
To remove these deficiencies, you need objects that:
Function return values satisfy these criteria, since they are unnamed temporary objects created by a function call and accessible only to the caller. Once the call is complete, the caller may check or copy the returned object's value; after that, the original returned object evaporates, and hence can't be reused. And because the object is unnamed, it can't be hidden.
(For C++ I am assuming rvalue function call expressions only, meaning calls that don't return references. As I'm restricting this part of my discussion to C-compatible techniques only, and as C lacks references, this assumption is reasonable.)
Return values themselves represent only Stage 2 of an exception's lifetime. However, in conjunction with both the calling and called functions, they are often part of a complete exception implementation:
int f()
{
int error;
/* ... */
if (error) /* Stage 1: error occurred */
return -1; /* Stage 2: generate exception object */
/* ... */
}
int main(void)
{
if (f() != 0) /* Stage 3: detect exception */
{
/* Stage 4: handle exception */
}
/* Stage 5: recover */
}
Return values are the Standard C Library's favorite method for propagating exceptions. Consider these stereotypical examples:
if ((p = malloc(n)) == NULL)
/* ... */
if ((c = getchar()) == EOF)
/* ... */
if ((ticks = clock()) < 0)
/* ... */
Note the classic C idiom of both capturing the returned value and testing for an exception in one statement. This economy of expression comes by overloading one channel (the returned object) with two distinct meanings: legitimate data value and exception value. Code must interpret the channel both ways until it knows which is correct.
The notion of functions returning values is common to many languages, a fact Microsoft exploits with their language-independent Component Object Model (COM). COM methods notify you of exceptions via returned objects having type HRESULT, Microsoft-ese for specially formatted 32-bit unsigned values. Unlike the examples just discussed, COM return values carry only status and exception information; other outbound information comes through pointer parameters.
Outbound pointer and C++ reference parameters are a variation on function return values, with a few distinct differences:
This wraps up my general introduction to exceptions and their traditional support within Standard C. In Part 2 I'll explore Microsoft's extension to these Standard C methods: special exception-handling macros, and Structured Exception Handling, or SEH. I will also summarize the limitations in all the C-compatibles approaches (including SEH), thereby setting the stage for C++ exceptions in Part 3.
Robert Schmidt is a technical writer for MSDN. His other major writing distraction is the C/C++ Users Journal, for which he is a contributing editor and columnist. In previous career incarnations he's been a radio DJ, wild-animal curator, astronomer, pool-hall operator, private investigator, newspaper carrier, and college tutor.
The American National Standards Institute (http://www.ansi.org/) technical committee, once designated X3J11 but now known simply as J11 (http://www.x3.org/tc_home/j11.htm), is responsible for creating and maintaining the U.S. C programming language Standard. A similar committee, J16 (http://www.x3.org/tc_home/j16.htm), is responsible for the collateral U.S. C++ Standard. Both committees represent the U.S. on the corresponding ISO committees.
The C language specified in ANSI's 1989 C Standard, and largely inherited from K&R C. Also known as C89 (from its year of adoption).
Formal document name: ANSI X3.159-1989. The U.S. C language Standard published by ANSI in 1989, technically equivalent to—and supplanted by—the ISO C Standard published a year later.
Acronym for The Annotated C++ Reference Manual, written by Margaret Ellis and Bjarne Stroustrup, and first published in 1991. The ARM is to Standard C++ as K&R is to Standard C: The de-facto standard of its day, and the foundation for the eventual ISO Standard.
A C or C++ local-scope object explicitly declared auto or register, or not explicitly declared extern or static, has automatic storage duration. An object with such storage duration is an automatic object. Storage for these objects lasts until the block in which they are created exits. Automatic objects are what most programmers think of as "local variables" or "stack variables."
Standard-ese for the const and volatile type qualifiers.
Acronym for The Design and Evolution of C++, written by Bjarne Stroustrup, and first published in 1994. While the C and C9x Standards have corresponding Rationale documents, the C++ Standard does not. Instead the D&E serves as the de-facto Rationale for Standard C++.
A C++ object is dynamically created via a new expression, and dynamically destroyed via a delete expression. Such objects have dynamic storage duration; their storage lasts until freed by operator delete or operator delete [].
Shorthand notation for Standard C++ exception handling.
A C++ object is fully constructed if, and only if, its constructor has completed and its destructor has not begun. Full construction is aborted if the constructor throws an exception.
Since contained subobjects construct before the containing constructor begins, the property of full construction is recursive: If one subobject deep in a class hierarchy throws an exception, all nested containing objects up the chain fail to construct unless/until the exception is handled. Caveat Constructor.
Also known as the International Organization for Standardization (http://www.iso.ch/). Contrary to popular belief, the name ISO is not an acronym, but derives from the Greek word "isos," meaning "equal." (The English prefix "iso-" also derives from this same word.)
ISO committees JTC1/SC22/WG14 and WG21 (http://anubis.dkuug.dk/JTC1/SC22/WG21/) are responsible for creating international C and C++ language Standards, respectively. The committees comprise representatives from national standards bodies; in the United States, those national bodies are ANSI committees J11 and J16.
Formal document name: ISO/IEC 9899:1990. There is also a corresponding Rationale.
This document is the international C language Standard published by ISO in 1990. It is technically identical to the ANSI C Standard; however, the two published Standards use slightly different nomenclature and section numbering.
Since 1990, ISO has updated the C Standard with three addenda:
ISO/IEC 9899 AM1 (http://www.lysator.liu.se/c/na1.html). 1995 Amendment 1, adding international character support. Also known as Normative Addendum 1.
ISO/IEC 9899 TCOR1 (http://anubis.dkuug.dk/JTC1/SC22/WG14/www/docs/tc1.htm). 1995 Technical Corrigendum 1, correcting technical errors in the Standard.
ISO/IEC 9899 TCOR2 (http://anubis.dkuug.dk/JTC1/SC22/WG14/www/docs/tc2.htm). 1996 Technical Corrigendum 2, correcting a smaller number of additional technical errors.
The C Standard is not free, nor can you purchase it from ISO. You must instead purchase it from either your nation's ISO member body (http://www.iso.ch/addresse/membodies.html) or a reseller.
ISO C9x Standard (Final Committee Draft)
Formal document name: WG14/N843. There is also a corresponding Rationale (ftp://ftp.dmk.com/DMK/sc22wg14/rationale/c9x/).
ISO working group JTC1/SC22/WG14 is currently revising the entire C Standard. The C language specified by this revised Standard is colloquially called C9x. As the name suggests, C9x standardization is scheduled for completion in the 1990s.
Formal document name: ISO/IEC 14882:1998. This document is the international C++ language Standard published by ISO in 1998.
As with the C Standard, you must purchase the C++ Standard. Fortunately the 1997 Final Committee Draft (http://www.maths.warwick.ac.uk/cpp/pub/wp/html/cd2/), which is freely available online, is mostly identical to the actual Standard. If you decide to purchase the Standard, I recommend you save money and trees: The paper version is ten times the cost of the electronic version.
Shorthand for Brian Kernighan and Dennis Ritchie's book, The C Programming Language (http://www1.fatbrain.com/asp/bookinfo/bookinfo.asp?theisbn=0131103628), first edition. Published in 1978, K&R formed the basis of the ANSI C Standard introduced a decade later.
The C language specified in K&R. K&R C lacks several key features of Standard C: function prototypes, const and volatile keywords, void type, enumeration types, and a well-defined library.
Literally an "l value" or "left value." So-called because, in K&R C, an lvalue can appear on the left side of an assignment expression. In Standard C or C++, an lvalue is more properly a "locator value" designating an object.
Lvalues are either modifiable or non-modifiable, a concept generally mapping to non-const and const objects, respectively. Their names aside, non-modifiable (const) lvalues cannot appear on the left in an assignment. (Because K&R C does not have the const keyword, all K&R lvalues are modifiable.)
Encoding of C++ names into C pseudo-names discernible by C linkers. In particular, such mangling allows C linkers to support class/namespace scope and function overloading, by turning what would be invalid redefinition of the same name into a new definition of a unique synthetic name.
The C++ Standard does not regulate the algorithm mapping between original C++ names and linker-friendly mangled names. Instead, each translator vendor is free to create a unique naming scheme. This suggests that object files with different name-mangling schemes cannot be mixed.
(Note that other considerations—parameter-passing method, register allocation, stack alignment—also prevent inter-vendor object file mixing. Name mangling just takes a bad interoperability situation and makes it worse.)
A C++ object is partially constructed if, and only if, its constructor has not finished execution.
Literally "Run-Time Type Identification," the Standard C++ mechanism for class objects to identify their dynamic or run-time types. RTTI is supported by the keywords dynamic_cast and typeid and the Standard Library header <typeinfo>. An object's run-time identity is typically stored as data accessed through the object's v-table.
Literally an "r value" or "right value." Rvalues always appear on the right side of an assignment statement. Unlike lvalues, which designate objects, rvalues designate values only—at least in C. C++ also has "class rvalues" designating unnamed temporary objects of a constructed (class) type; such objects are conceptually values of that type.
The C language specified in the ISO C Standard. Technically identical to ANSI C.
The C++ language specified in the ISO C++ Standard.
In Standard C, any of the keywords
In Standard C++, the above keywords plus
As the term suggests, a storage class specifier generally describes what kind of storage objects occupy, how long that storage exists, and how visible the storage is. The anomaly is mutable, which really doesn't describe storage at all. However, since mutable can appear in exactly the same grammatical contexts as the other "real" storage class specifiers, considering mutable a storage class specifier does simplify the C++ grammar.
A C++ object contained by other objects. A subobject is a named data member object, an unnamed base class object, or an array element.
Ant. complete object
To the CV qualifiers const and volatile, C9X adds a third: restrict. Rather than call the resulting set of attributes "CRV qualifiers" or some such, the C9X Standard committee elects to call them simply "type qualifiers."