Qualifiers in multilevel pointers - Embedded.com

Qualifiers in multilevel pointers

In my previous column, I explained why the qualification conversion rules in C and C++ are as they are.1 At the time, I discussed only single-level pointers–pointers with types of the form “pointer to T” for any type T other than another pointer type. In this column, I'll expand the discussion to pointers with multiple levels of indirection–pointers of the form “pointer to pointer to T” or “pointer to pointer to pointer to T”, and so on. First, however, we need to review some terminology.

In C++, the keywords const and volatile are called cv-qualifiers . A cv-qualified type has the form “cv T” where cv is a sequence of cv-qualifiers and T is a type without cv-qualifiers. The sequence cv can be empty, just const by itself, just volatile by itself, or both const and volatile in either order. For any two sequences cv1 and cv2 , if every cv-qualifier in cv1 also appears in cv2 , we say that “cv2 is no less cv-qualified than cv1 ” and write either:

cv2  >= cv1 cv1  <= cv2   

If cv1 and cv2 are different and cv2 is no less cv-qualified than cv1 , we say that “cv2 is more cv-qualified than cv1 ” and write either:

cv2  > cv1 cv1  < cv2   

For example, these are true:

empty  < constconst < const volatile  

but these are not:

const < volatilevolatile < const  

The C standard uses slightly different terminology than what I just described, but the concepts are the same.

A qualification conversion is a conversion from an expression of type “pointer to cv1 T” to an expression of type “pointer to cv2 T” where cv2 &#62 cv1 . For example, the following are valid qualification conversions:

• From T * to T volatile *

• From T * to T const volatile *

• From T const * to T const volatile *

• From T volatile * to T const volatile *

but these are not:

• From T volatile * to T const *

• From T const * to T volatile *

• From T volatile * to T *

• From T const volatile * to T volatile *

Armed with these concepts, we can tackle the conversion rules for multilevel pointers. But first, let's look at an example using a two-level pointer.

Converting enumerations to and from text
As I explained in an earlier column, neither C nor C++ provides symbolic output for enumerations.2 That is, given:

enum day    {    day_begin,    Sunday = day_begin, Monday, ..., Saturday,    day_end    };typedef enum day day;...day d = Monday;  

it would be nice if C programs could display the value of d as Monday by simply writing something such as:

printf("%e", d);    // in C  

However, printf doesn't provide a format specifier for enumerations. (The %e format specifier is already taken for floating pointer numbers.) Although C++ programs can display day d using:

std::cout << d;     // in C++  

by default it displays d as a number rather than as a symbolic name.

You can implement symbolic output for an enumeration by using a translation table such as:

char *day_image[] =    {    "Sunday", "Monday",... "Saturday", "???"    };      

Using this array, day_image[d] yields an NTCS (null-terminated character sequence) whose value is a textual representation of the value of d . Then you can use either:

printf("%s", day_image[d]); // in C  

or:

std::cout << day_image[d];  // in C++  

to display the value of d as a character sequence. C++ lets you take this one step further–you can overload the &#60&#60 operator so that calling:

std::cout << d;             // in C++  

transforms d into day_image[d] implicitly.

Input for enumerations requires a bit more work. You must read the textual representation for an enumeration value into a character array as an NTCS, and then convert that NTCS to an enumeration value. In C++, you might use a string object instead of an NTCS, but I'll stick with the NTCS for this example.

To convert the NTCS to an enumeration, you might define a function named index such that calling index(n, t) returns the integer-valued index of NTCS n within table t . Then you can use an expression such as:

(day)index(s, day_image)  

to convert NTCS s to a day.

You can declare index as either:

int index(char *n, char *t[ ]);int index(char *n, char **t);  

which are equivalent. Let's work with the latter.

Factoring in const
Calling index(n, t) should change neither n nor t . Both parameters should be declared with const . At the very least, the function declaration should look like:

int index(char const *s, char const **t);  

This would be fine, if only it worked. Given:

char n[...];char *day_image[] = { ... };  

then calling index(n, day_image) won't compile because it can't convert day_image to the type of the second parameter, t . Strictly speaking, day_image has type “array of pointer to char.” The compiler transforms it to “pointer to pointer to char” as it evaluates the call expression, but it can't convert it to type of the parameter t , which is “pointer to pointer to const char.” Although for any type T , C and C++ permit conversion from T * to T const * , they don't permit conversion from T ** to T const ** .

Why the conversion is unsafe
Here's the canonical example from the C++ Standard that illustrates why C++ does not permit this conversion. If a program could convert a T ** to a T const ** , as on line 3:

(1) char const c = 'x';(2) char *p;(3) char const **pc = &p; // ?(4) *pc = &c;(5) *p = 'y';  

then this code would compile and, when it runs, alter the constant object c . Let's examine this example in detail to see how this could happen.

The first statement:

(1) char const c = 'x';  

declares c as a character whose initial value 'x ' should never change.

The next statement:

(2) char *p;  

declares p as a pointer to a character that could be changed. This means that p should never point to c .

The next statement contains the questionable conversion:

(3) char const **pc = &p;  

Assuming that this statement is valid, then at this point, *pc is an alias for p ; however, *pc has type char const * and p has type char * . No damage has occurred . . . yet.

As I noted earlier, p should never point to c , but if *pc is an alias for p , then:

(4) *pc = &c;  

has the same effect as assigning p = &c . That is, (4) has the same effect as:

p = (char *)&c;  

but without the honesty of the explicit cast. Still no damage has occurred.

The final statement:

(5) *p = 'y';  

does the damage. Since p now points to c , this tries to overwrite the value of c . Since c is defined as const, this assignment has undefined behavior.

Fortunately, we never get to this point because the code doesn't compile. The compiler rejects the attempted conversion on (3).

Back to the original example
In most circumstances, you should declare day_image as:

char const *const day_image[] =    {    "Sunday", "Monday",... "Saturday", "???"    };  

That is, every pointer in the array should be const, and each of those pointers should point to characters that are const.

If you want index to be able to search this array, you must declare the function as:

int index(char const *s, char const *const *t);  

Then calling index(n, day_image) works just fine because the types of argument day_image and parameter t match exactly.

On the other hand, I can imagine circumstances (albeit rare) in which you might want to define day_image as any of:

(a) char	*	day_image[] = ...;(b) char const	*	day_image[] = ...;(c) char	*const	day_image[] = ...;  

Can you still call index(n, day_image) using each of these?

For (b), the answer is unconditionally “yes.” When you pass day_image as a function argument, the compiler applies an array-to-pointer conversion:

char const *[]    →    char const **  

When you match this type against the parameter type, you can see that the conversion is actually just a qualification conversion on a single-level pointer:

char const *	*    ↑    ↓char const *const	*  

In other words, it's just a conversion from T * to T const * where T is char const * . Now, what about:

(a) char	*	day_image[] = ...;(c) char	*const	day_image[] = ...;  

Can you still call index(n, day_image) using each of these? You can in C++, but you can't in C.

C++ vs. C
C++ permits qualification conversions on multilevel pointers only if the pointers are similar . Two pointer types T1 and T2 are similar if, for some type T and an integer n &#62 0:

• T1 is T cv1 ,n * … cv1,1 * cv1,0

• T2 is T cv2,n * … cv2,1 * cv2,0

where each cvi,j is a cv-qualifier sequence. In other words, two pointer types are similar if:

• They have the same number of *s as each other, and

• The leftmost * of each type points to the exact same type T

Formally, C++ permits conversion from pointer type T1 to pointer type T2 if and only if (hold on to your hat):

• T1 and T2 are similar, and

• For every j &#62 0, cv2,j &#62= cv1,j (cv2,j is no less qualified than cv1,j ), and

• If, for some j , cv1,j and cv2,j are different, then for each k such that 0 &#60 k &#60 j , every cv2,k includes const

Whew! Let's say it another way. C++ permits conversion from pointer type T1 to similar pointer type T2 if and only if:

• Every cv-qualifier in T1 also appears in the corresponding position in T2, and

• If T2 has a cv-qualifier that's not in T1, then every * in T2 to the right of that cv-qualifier, except the last one, must also be const-qualified

Thus, these are valid conversions in C++:

char **          →    char const *const *char **          →    char volatile *const *char ***         →    char *volatile *const *char *const **   →    char const *const *volatile const *  

These are not:

char **     →    char volatile **char **     →    char const volatile **char ****   →    char *const volatile **const *  

Although some C compilers allow C++ compatible qualification conversions on multilevel pointers, Standard C doesn't. In Standard C, if you declare day_image as either:

(a) char	*	day_image[] = ...;(c) char	*const	day_image[] = ...;  

then a call such as index(n, day_image) won't compile.

If you really want it to compile, you must use a cast, as in:

index(n, (char const *const *)day_image)  

Using cast is usually an indication that you're doing something that's unsafe, but in this case, the conversions are safe. The conversion rules of Standard C just don't acknowledge that they are.

Dan Saks is president of Saks & Associates, a C/C++ training and consulting company. For more information about Dan Saks, visit his website at www.dansaks.com. Dan also welcomes your feedback: e-mail him at .

Endnotes:
1. Saks, Dan,
“Volatile as a promise”, Embedded Systems Design , January 2006, p. 15. Back

2. Saks, Dan, “More on Enumerations”, Embedded Systems Programming , July 2003, p. 38. Back

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.