P.J. Plauger has been a prolific programmer, textbook author, and software entrepreneur. He is secretary of the ANSI C standards committee, X3J11, and convenor of the ISO C standards committee. His latest book is Standard C which he co-authored with Jim Brodie.
Last month, I described the first meeting of committee X3J11 since the C standard was approved by ANSI. We are now into the interpretation phase. People who have questions about the standard can submit them to X3. That committee registers the questions and passes them to X3J11 to draft a response.
I outlined the rules and procedures by which X3J11 must now respond to requests for interpretation. I also discussed a handful of questions that were handled at the first interpretation meeting. The questions were presented in order of difficulty.
Even the most difficult of the questions I presented last month merely involved lapses in the standard. The issues arose because the standard failed to describe certain cases completely. Absent specific details, it devolves upon X3J11 to provide an interpretation.
This month, I present several issues that are nastier. They arise because the standard doesn't say what many of us intended it to say. That may be because we didn't understand the import of certain phraseology. It may be because of an infelicitous edit that got by the extensive review process. It may be because we simply got it wrong.
The reasons why are now largely unimportant. The C standard says what it says. We cannot change it until the next review process, years from now. We can only interpret what it says, like it or not.
Here are a few cases where a number of us definitely do not like what the standard says. I will discuss the issues that turned up the unexpected wording. I will also endeavor to estimate the extent of the damage, given that we have to live with the words we adopted.
Percolating Type Information
We received a request for clarification of how declarations with linkage interact. Linkage can be external, for a name known across separate translation units, or internal, for a "file level static" known only within the current translation unit. A declaration can also have no linkage, such as an auto or typedef declaration.A declaration with linkage designates the same function or data object as any other declaration with linkage for the same name in the same translation unit. For example,
extern int a[]; extern int a[10];Both declarations have external linkage and both declare the name a. Therefore, both name the same array of int. The types are not identical, but they are compatible. For the second declaration, the translator ascribes to a the composite type formed from the two compatible types. A composite type merges all information from the two types. In this case, the composite type is array of 10 int.All that is clear enough and familiar to most C programmers. Where life gets exciting is when the two declarations exist in different scopes. Existing C translators are all over the map on this topic. Committee X3J11 had innumerable discussions to determine the best standard behavior.
One thing we eventually agreed upon was that type information should not percolate out from an inner block. For example,
extern int a[]; int f() { extern int a[10]; .....} int sizea = sizeof a; /* ERROR */Both declarations designate the same array a, but only the inner one knows the size of the array. That declaration evaporates when its scope ends. Hence, the operand to the sizeof operator has an incomplete type. Since this violates a constraint error, the translator must issue a diagnostic.You know and I know that the array has ten elements. Any translator can easily retain the same information. It probably should, for checking of further declarations or for passing hints on to the linker. Nevertheless, it cannot make use of this information to avoid the diagnostic. That would do violence to the block structure of C. A few of us fought against such institutionalized priggishness, but in the end we went along.
Now for the problem. What happens if we turn the situation around? Consider the following:
extern int a[10]; int f() { extern int a[]; int sizea = sizeof a; /* ERROR! */ .....}This does not do violence to the block structure of C. The first declaration of a is unequivocally visible when the second one appears. It would be only sensible to use the composite type as the effective type of the second declaration. But that is not what the standard says.The relevant statement is in Section 3.1.2.6, page 26, lines 19-20: "For an identifier with external or internal linkage declared in the same scope as another declaration for that identifier, the type of the identifier becomes the composite type." Note that the earlier declaration must not be just visible, but in the same scope. Lest there be any doubt, Section 3.1.2.1, page 21, line 38 says: "Two identifiers have the same scope if and only if their scopes terminate at the same point."
That is not the behavior many of us signed up for. We wanted type information to percolate in from outer blocks, even if it could no longer percolate out from inner blocks. We didn't want translators to have to play stupid when they obviously knew better.
In particular, we wanted the assurance that the old practice of "importing" externals would not be penalized. With this style of coding, you write extern declarations at the top of each function body to advertize what resources it uses. That lets you move functions about with less concern that the appropriate declaration context occurs before each function definition.
If type information does not percolate in, however, you lose some of the advantage of new features in Standard C. Consider:
double sin(double); int f() { extern double sin();The function prototype is shielded within the function body by the old-style declaration. No argument type checking or coercion occurs. Similarly, an incomplete type remains incomplete even if the full type information is visible, as in the earlier example. This is hardly desirable behavior for a language with a long-standing reputation for pragmatism.The actual wording of the standard may not be what we all wanted, but it is by no means a disaster. It should encourage you to go hunting for extern declarations inside function bodies. Get rid of them, or at least move them outside any functions. Forget any old notions about making each function definition self-sufficient enough to be easily moved. Instead, focus on declaring everything at the top of each source file. That's where your #include directives belong anyway.
Whatever your earlier vision of C, remember that it is now very much a block structured language. You can still do violence to that block structure by writing declarations with linkage. That violence is much more contained, however.
Percolating Storage Class Information
I'll just touch briefly on a related gaffe. It seems we interfered with percolating storage classes in much the same way we messed up type information. Consider the following:
static int x; int f() { extern int x: { extern int x; /* ERROR! */We all know how to read the first extern declaration. It doesn't mean that x has external linkage, because an earlier declaration for x is visible that has linkage. In this case, the extern storage class keyword means "whatever you said earlier." So the declaration refers to the same x as before, which has internal linkage.You'd think that the same principle applies to the second extern declaration. So did many of us on the committee. But we are all wrong. The innermost declaration is shielded from the outermost one in a surprising way.
Section 3.1.2.2, page 22, lines 16-19 say: "If the declaration of an identifier for an object or a function contains the storage-class specifier extern, the identifier has the same linkage as any visible declaration of the identifier with file scope. If there is no visible declaration with file scope, the identifier has external linkage."
Note that the standard talks about a visible declaration with file scope. It does not refer to a visible declaration with linkage. That's probably what most of us meant, but it's not what the standard says.
In the example above, no declaration with file scope is visible to the innermost declaration. It is shielded by the intervening declaration. That means the innermost declaration is for an x with external linkage. Since you cannot specify both internal and external linkage for the same name in one translation unit, the program is in error. (It is undefined behavior, so a translator does not necessarily have to emit a diagnostic.)
Put in plain English, storage class information doesn't percolate in. Maybe it should, but it doesn't.
Knothole Type Casts
Another request for clarification concerned floating point comparisons. It is always a touchy business testing two floating point values for exact equality. Why a prudent programmer would trust any such comparison is beyond me. Nevertheless, people have the right to ask about guarantees. And the commitee has the obligation to answer as best it can.I won't reproduce all of the fringe cases the committee felt obliged to examine. Instead, I will focus on a particular area where the wording of the standard caused another of those nasty surprises. The consequences may even spread beyond the narrow business of floating point comparisons.
One of the roots of the problem goes back to the earliest days of C. The PDP-11, on which C was first implemented, has an optional floating point processor, or FPP. That processor encouraged you to perform all calculations with double values. It was in many ways easier to convert float operands to double and do the operation than to switch the FPP to float mode and leave the operands unconverted. It also happened to retain more precision at only a relatively small performance penalty.
That is why C used to specify that all floating point arithmetic is performed with double values. It is one of the reasons why float arguments are passed to functions as type double. The C standard has eliminated the conversion requirement for floating point arithmetic. It relaxes the argument passing requirement in the presence of a function prototype. But it still permits a certain latitude.
Even modern implementations sometimes prefer to do arithmetic at a greater precision than called for by the typing rules of C. The Intel 80X87 chips perform arithmetic internally with 80-bit representations, for either 32-bit float or 64-bit double operands. Much the same is true of the Motorola 68881. You can force any of these chips to discard extra precision from time to time, but who wants to? It is difficult, wasteful, and silly to trash those extra bits.
The issue isn't even confined to floating point values. Computers seldom have a full complement of divide or shift instructions. An implementation often has to perform these operations to greater precision than required by C. That lets you get a "righter" answer, one that avoids a potential intermediate overflow, in some integer expressions. There are even implementations that perform some integer operations in floating point, because the FPP is so fast.
All of those considerations led to a special dispensation that was added to the standard. Section 3.2.1.5, page 36, lines 38-39 say: "The values of floating operands and of the results of floating expressions may be represented in greater precision and range than that required by the type; the types are not changed thereby." Note that integer expressions are not included. An implementation can avoid overflow by keeping extra intermediate bits. It cannot keep extra bits that the programmer really wants to have scraped off.
Now let's get back to the business of comparing floating point values for equality. With all that extra precision hanging about, guarantees of any sort are hard to come by. Remember that the extra precision is optional. The same expression written in two different places can be evaluated different ways. You can write all sorts of algebraic equalities that get sabotaged by a zealous C optimizer.
C does have a way to force a given representation for a value, however. If you assign the value of an expression to a data object, the value must be converted as needed to fit in that data object. Any excess bits get scraped off. Thus,
double x, y; x = 1.0 / 3.0; y=x; if (x == 1.0 / 3.0) /* MIGHT NOT BE TRUE */; if (x == y) /* MUST BE TRUE */;It is not a permissible optimization to replace either x or y in an expression with the unconverted value that gets stored. You can use only the value after conversion. C programmers have relied on this behavior for years. You use it to pick the float bits out of a double intermediate value. You use it to pick the char part out of an int. You often depend on assignment to give a known representation for a value.There is also another way to force a given representation. Most of us have learned to use a type cast in place of an assignment. It saves making up some temporary data object, and it can save a needless store and access.
Not all early C compilers let you scrape bits with a type cast. If the cast specified a type narrower than one of the computational types, it was "widened" to int or double. I was guilty of writing a family of compilers that worked that way. I am quick to admit that it is not the most desirable behavior. You generally want a type cast to behave like a knothole and scrape off any bits not used to represent the specified type.
The committee agreed that C should have knothole casts. We did not say this as well as we should have, however. Section 3.3.4, page 46, lines 24-26 say: "Preceding an expression by a parenthesized type name converts the value of the expression to the named type. This construction is called a cast. A cast that specifies no conversion has no effect on the type or value of an expression."
That last sentence is the killer. It raises serious questions about how good a knothole a type cast really is. Sure, you still have to scrape bits if the cast calls for a type different from that of its operand. But what if the type is the same? That is indeed a cast that specifies no conversion. It must therefore have no effect on the type or value of an expression.
Sounds innocent enough until you think about those extra bits drifting around in intermediate calculations. Then you wonder whether an implementation is required to scrape them off. That causes trouble for expressions like:
double x = 1.0 / 3.0; if (x == (double)(1.0 / 3.0)) /* MIGHT NOT BE TRUE */;If the words are to be taken literally, the (double) type cast may have no effect. This is clearly not what was desired.It will be hard for the committee to interpret its way out of this one. Driving another nail in the coffin is another sentence in Section 3.2, page 35, lines 7-8: "Conversion of an operand value to a compatible type causes no change to the value or the representation." That is practically a restatement of the same constraint in the description of type casts.
Let me emphasize that the committee has made no official interpretation of this issue to date. It is still possible for it to justify the interpretation most of us want. It is a foolish implementor who would take advantage of this latitude and supply broken knothole casts. Nevertheless, here is another place where the wording of the standard is far from perfect.
The Thin Ice Never Ends
I can recall only one other area where the committee has found poor wording (so far). That is in the description of function prototype parameters and how they parse. It deals with an ambiguity that has been in C in various forms since typedef was invented. Dennis Ritchie had troubles with this issue. It seems that the trouble continues to haunt us to this day.Language designers like context free languages. It's nice to be able to parse any context irrespective of what has gone before. When C was first born, it was a context free language. Once typedef was added, however, that went out the window.
What typedef does is to change the syntactic category of certain names. Where once they could name only operands in an expression (outside of declarations), now they could name type parts as well. A C parser takes quite a different attitude toward a type part than it does toward, say, a name declared as a data object. A type part signals the beginning of a new declaration, in a variety of contexts.
To parse C, you have to know what typedef names are visible in each context. Given the block structure of the language, that is not a job you can do by halves. A parser really has to understand the semantics of all declarations in order to progress correctly through a translation unit.
What makes matters worse is a small assortment of ambiguities. C is a language that encourages you to leave out obvious bits. Omit a type specifier and the translator assumes you want type int. Omit a storage class and the translator guesses extern or auto, depending upon the context of the declaration. That is a latitude we all tend to enjoy when we write C code. (When was the last time you wrote the keyword auto?)
Mix abbreviated declarations with typedef names and you're asking for trouble. Consider:
typedef int T, U; int f() { extern T(U);That last declaration can have (at least) two different meanings. It can declare T to be a function returning (implicitly) int with a single argument of type U. Or it can declare that U is an external data object of type T. Which is it to be?Dennis Ritchie established the precedent for dealing with such ambiguities. (At the same time, he admitted that the language is spongy in this area with his famous statement, "The ice is thin here.") Ritchie gave a simple rule. If a name is visible as a typedef and a type part is valid, then the name should be taken as a typedef. This is true even if an alternate interpretation is valid. It remains true even if subsequent parsing reveals that the typedef interpretation leads to a syntax error.
One place the ice is thin is when you want to redeclare a typedef name in an inner block. You can make the ice thicker by avoiding abbreviations in the declaration that gives the name a new meaning. Don't give the parser, or a casual reader, the opportunity to misunderstand your intention.
Another place is when you write redundant parentheses within a declarator. Every time the parser sees a left parenthesis within a declaration, it has to make a guess. Are we about to see a chunk of stuff that is parenthesized to get the grouping right? Or are we about to see a parameter list for a function declaration? Sometimes you can, or must, omit the name in a declaration. That only adds to the excitement for the parser.
Ritchie resolved this ambiguity by fiat as well. He said that empty parentheses within a declarator should always be taken as signalling the attribute "function returning." They are never treated as redundant parentheses around an omitted name.
So another good rule is never to write redundant parentheses in a declarator. You are only inviting misunderstanding if you do.
There were just a couple of places in C where Ritchie's rules had to be invoked. When the committee added function prototypes, we introduced one or two more places. We agreed from the start to resolve any new ambiguities the same ways Ritchie did. The only problem is, we didn't say what we meant as clearly as we should have.
What we said instead was in Section 3.5.4.3, page 69, lines 2-4: "In a parameter declaration, a single typedef name in parentheses is taken to be an abstract declarator that specifies a function with a single parameter, not as redundant parentheses around the identifier for a declarator."
That takes care of the simplest ambiguities, but not all of them. Consider:
typedef int T, U; int f(T (U(V)));Don't try to parse this. It will only give you a headache. What you need to know is that it gives X3J11 a headache as well. It is an ambiguous case that is not resolved by the language of the standard.This situation is far from disastrous. I know of no controversy over what the committee intended, or of what behavior everyone else wants. We just didn't say it right in the standard. We can't argue that the omission is a typographical error. We are not permitted to insert "correct" wording at this juncture. We will simply have to produce an interpretation and send it through X3 channels for approval.
Conclusion
I don't think that any of these gaffes constitute serious damage to the C standard. On the contrary, I am still pleasantly surprised that X3J11 got so much right. I recite the problems here to give you a flavor of how the interpretation phase is progressing for the C language. I also hope to show you that the standard still seems to be in pretty good shape.My hope and expectation is that it will remain in good shape under continued scrutiny.