Translation Limits Every ANSI C compiler is required to support at least: • 31 parameters in a function definition • 31 arguments in a function call • 509 characters in a source line • 32 levels of nested parentheses in an expression • The maximum value of long int can't be any less than 2,147,483,647, (i.e., long integers are at least 32 bits). and so on. Furthermore, a conforming compiler must compile and execute a program in which all of the limits are tested at once. A surprising thing is that these "required" limits are not actually constraints—so a compiler can choke on them without issuing an error message. Reading the ANSI C Standard for Fun, Pleasure, and Profit Sometimes it takes considerable concentration to read the ANSI C Standard and obtain an answer from it. A sales engineer sent the following piece of code into the compiler group at Sun as a test case. 1 foo(const char **p) { } 2 3 main(int argc, char **argv) 4{ 5 foo(argv); 6} If you try compiling it, you'll notice that the compiler issues a warning message, saying: line 5: warning: argument is incompatible with prototype The submitter of the code wanted to know why the warning message was generated, and what part of the ANSI C Standard mandated this. After all, he reasoned, argument char *s matches parameter const char *p This is seen throughout all library string functions. So doesn't argument char **argv match parameter const char **p ? The answer is no, it does not. It took a little while to answer this question, and it's educational in more than one sense, to see the process of obtaining the answer. The analysis was carried out by one of Sun's "language lawyers," [6] and it runs like this: [6] The New Hacker's Dictionary defines a language lawyer as "a person who will show you the five sentences scattered through a 200-plus-page manual that together imply the answer to your question 'if only you had thought to look there.'" Yep! That's exactly what happened in this case. The Constraints portion of Section 6.3.2.2 of the ANSI C Standard includes the phrase: Each argument shall have a type such that its value may be assigned to an object with the unqualified version of the type of its corresponding parameter. This says that argument passing is supposed to behave like assignment. Thus, a diagnostic message must be produced unless an object of type const char ** may be assigned a value of type char **.To find out whether this assignment is legal, flip to the section on simple assignment, Section 6.3.16.1, which includes the following constraint: One of the following shall hold:? ?Both operands are pointers to qualified or unqualified versions of compatible types, and the type pointed to by the left has all the qualifiers of the type pointed to by the right. It is this condition that makes a call with a char * argument corresponding to a const char * parameter legal (as seen throughout the string routines in the C library). This is legal because in the code char * cp; const char *ccp; ccp = cp; The left operand is a pointer to "char qualified by const". The right operand is a pointer to "char" unqualified. The type char is a compatible type with char, and the type pointed to by the left operand has all the qualifiers of the type pointed to by the right operand (none), plus one of its own (const). Note that the assignment cannot be made the other way around. Try it if you don't believe me. cp = ccp; /* results in a compilation warning */ Does Section 6.3.16.1 also make a call with a char ** argument corresponding to a const char ** parameter legal? It does not. The Examples portion of Section 6.1.2.5 states: The type designated "const float *" is not a qualified type--its type is "pointer to const-qualified float" and is a pointer to a qualified type. Analogously, const char ** denotes a pointer to an unqualified type. Its type is a pointer to a pointer to a qualified type. Since the types char ** and const char ** are both pointers to unqualified types that are not the same type, they are not compatible types. Therefore, a call with an argument of type char ** corresponding to a parameter of type const char ** is not allowed. Therefore, the constraint given in Section 6.3.2.2 is violated, and a diagnostic message must be produced. This is a subtle point to grasp. Another way of looking at it is to note that: ?the left operand has type FOO2--- pointer to FOO, where FOO is an unqualified pointer to a character qualified by the const qualifier, and ?the right operand has type BAZ2---pointer to BAZ, where BAZ is an unqualified pointer to a character with no qualifiers. FOO and BAZ are compatible types, but FOO2 and BAZ2 differ other than in qualifica-tion of the thing immediately pointed to and are therefore not compatible types; therefore the left and right operands are unqualified pointers to types that are not compatible. Compatibility of pointer types is not transitive. Therefore, the assignment or function call is not permitted. However, note that the restriction serves mainly to annoy and confuse users. The assignment is currently allowed in C++ translators based on cfront (though that might change). We felt that a lot of people would have questions in the future, and not all of them would want to follow the process of reasoning shown above. Const Isn't
The keyword const doesn't turn a variable into a constant! A symbol with the const qualifier merely means that the symbol cannot be used for assignment. This makes the value re ad -onl y through that symbol; it does not prevent the value from being modified through some other means internal (or even external) to the program. It is pretty much useful only for qualifying a pointer parameter, to indicate that this function will not change the data that argument points to, but other functions may. This is perhaps the most common use of const in C and C++. A const can be used for data, like so: const int limit = 10; and it acts somewhat as in other languages. When you add pointers into the equation, things get a little rough: const int * limitp = &limit; int i=27; limitp = &i; This says that limitp is a pointer to a constant integer. The pointer cannot be used to change the integer; however, the pointer itself can be given a different value at any time. It will then point to a different location and dereferencing it will yield a different value! The combination of const and * is usually only used to simulate call-by-value for array parameters. It says, "I am giving you a pointer to this thing, but you may not change it." This idiom is similar to the most frequent use of void *. Although that could theoretically be used in any number of circumstances, it's usually restricted to converting pointers from one type to another. Analogously, you can take the address of a constant variable, and, well, perhaps I had better not put ideas into people's heads. As Ken Thompson pointed out, "The const keyword only confuses library interfaces with the hope of catching some rare errors." In retrospect, the const keyword would have been better named readonly. Even though the rules were changed, subtle bugs can and do still occur. In this example, the variable d is one less than the index needed, so the code copes with it. But the if statement did not evaluate to true. Why, and what, is the bug? int array[] = { 23, 34, 12, 17, 204, 99, 16 }; #define TOTAL_ELEMENTS (sizeof(array) / sizeof(array[0])) main() { int d= -1, x; /* ... */ if (d <= TOTAL_ELEMENTS-2) x = array[d+1]; /* ... */ } The defined variable TOTAL_ELEMENTS has type unsigned int (because the return type of sizeof is "unsigned"). The test is comparing a signed int with an unsigned int quantity. So d is promoted to unsigned int. Interpreting -1 as an unsigned int yields a big positive number, making the clause false. This bug occurs under ANSI C, and under K&R C if sizeof() had an unsigned return type in a given implementation. It can be fixed by putting an int cast immediately before the TOTAL_ELEMENTS: if (d <= (int) TOTAL_ELEMENTS - 2)
Advice on Unsigned Types
Avoid unnecessary complexity by minimizing your use of unsigned types. Specifically, don't use an unsigned type to represent a quantity just because it will never be negative (e.g., "age" or "national_debt"). Use a signed type like int and you won't have to worry about boundary cases in the detailed rules for promoting mixed types. Only use unsigned types for bitfields or binary masks. Use casts in expressions, to make all the operands signed or unsigned, so the compiler does not have to choose the result type. |