Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I enforce not modifying any part of a referred to variable?

When making a reference to something, it's possible to add extra const qualifiers so that the referred variable cannot be modified, like this:

int *ptr;
int const * const &rptr = ptr;
//ptr can't be changed and *ptr can't be changed

Or like this, with an array:

int arr[1];
int const (&rarr)[1] = arr;
//arr[0] can't be changed

Or even like this, with an array of pointers:

int *ptrarr[1];
int * const (&rptrarr)[1] = ptrarr;
//ptrarr[0] cannot be changed, but *ptrarr[0] can be

Why, then, can I not combine these and do this?

int *ptrarr[1];
int const * const (&why)[1] = ptrarr; //error

When attempting this, Clang 3.5 produces the following error, and GCC 4.8.1 a similar one:

error: reference to type const int *const [1] could not bind to an lvalue of type int *[1]

What is the correct way to protect all parts of a referred to array of pointers with const?

Note: This is an artificial example, but I hope this results in knowledge about the language that could come in useful later.

like image 528
chris Avatar asked Jan 19 '14 06:01

chris


1 Answers

This is a bit of a mind-bender, but here is what I think is at the core of these issues. Here are the "problematic" examples:

int *ptrarr[1];
int const * (&rptrarr)[1] = ptrarr;   // error (1)
int const * const (&why)[1] = ptrarr; // error (2)

Case (1)

First, I'll tackle the example given in the comment by Dan Nissenbaum, which is this:

int const * (&rptrarr)[1]

This is actually covered in section 4.4/4 of the standard, which describes the acceptable cv-qualification conversions when you have multiple layers of pointers or types. The important requirement stated is the following:

if the cv 1,j and cv 2,j are different, then const is in every cv2,k for 0 < k < j.

What this states is that as you peel off the layers of types, there must always be all-consts in the outer layers before you reach a cv-conversion (i.e. cv1,j and cv2,j are different). This is true for pointer conversions beyond the first layer (which is covered by pointer-conversions, not cv-conversions). In other words, this is OK:

int* p_a;
int const * p_b = p_a;  // OK

because the first conversion being done is a pointer-conversion (copies the address value), and the second conversion is a cv-conversion. While this is not correct:

int** p_a;
int const ** p_b = p_a;  // BAD

because here, the first conversion is still a pointer-conversion, which is only OK if the pointee type are compatible (have a valid cv-conversion between them). In this case, it requires are cv-conversion of this non-const pointer to a non-const int, to a non-const pointer to a const int, and that is forbidden by the quoted rule of section 4.4/4 because the first layer is not const while the second layer requires a cv-conversion (from non-const int to const int).

I know that this is all confusing, but think about it for a while and it will become clear. Now, the reason why this rule exists in section 4.4/4 is because if you were allowed to do this, then I could do this:

int** pp_a;
int const ** pp_b = pp_a;  // let's say, the compiler accepted this..

int const * p_c;  // I have some pointer to a const int (that is really const).
pp_b[0] = p_c;    // the compiler would have to accept this too!!!

Clearly, the last line of this example cannot be acceptable in any way because it would be in complete violation of the cv-qualifications, i.e., it is a silent and implicit const-cast, while the whole purpose of these cv-conversion rules is to make sure that this is impossible.

At this point, it should be pretty obvious why int const * (&rptrarr)[1] = ptrarr; is not allowed, because the first layer is a reference, which obey essentially the same conversion rules as pointers, the second layer is a cv-conversion from a non-const array-of-pointers into a non-const array-of-pointers, and the last layer adds a const qualifier to the int type. And that is in direct violation of the rule that I quoted above.

Case (2)

Now, to the main question, which is much less clear, but I think it must have to do with the same argument, somehow. Just to restate, here is the case in question:

int const * const (&why)[1] = ptrarr; // error

The way I described the first case might be wrong by one layer, but I'm not sure, or maybe the compiler adds one layer too many. As I described it, the second const is needed because any conversion that comes before a non-const-to-const conversion must have a const type as a destination, that's the rule. Case (1) did not work because of those 3 layers of conversion. Here, if I apply the same logic with the layers of conversion, I get three "conversions": (1) from lvalue (ptrarr) to an lvalue-reference (why); (2) from non-const array-of-pointers to const array-of-pointers; and, (3) from non-const int to const int.

But here is the problem. What if the "array-of-pointers" cv-conversion is not just a single layer conversion step. If the compiler needs to split that into two layers, as in: (2-a) from non-const array to non-const array; and, (2-b) from non-const pointer to const pointer. Then, it is pretty obvious why this would, again, be in contraction with the rules in section 4.4/4. And under that hypothesis, all the cases you described and the 2 treated here are perfectly explained.

The problem now is that I couldn't find a place in the standard that would explain why this conversion needs to be split. The standard is pretty clear, in section 3.9.3/2, that cv-qualifiers do not apply to array types, and are, instead, carried over to the element type of the array. In other words, according to the standard, there is no difference between a "const array of T" and an "array of const T". And, therefore, this seems to contradict the standard.

It is possible that GCC and Clang take a bit of a short-cut and make an "array-reference" the same as a "pointer" (because it sort of is), or that they messed up a bit in the ordering of the conversions (which is unlikely). In any case, I could not find a definitive justification for this behavior in the standard. I hope someone does.

Another possible thing that could be at play here is alignment. As you probably know, elements of an array have to obey the alignment rules that pertain to them. If the alignment rules for int * are not the same as for int const * const (either because of the first or second const), then there is an obvious incompatibility, and hence, an error. Again, the standard says indeed that if the alignment rule is different there is a problem, but I could not find anything that suggests that a const pointer type would have a stricter (or different) alignment rule compared to a non-const pointer type, but it is not entirely impossible that there is such an incompatibility (knowing some of the archaic baggage that the C++ standard carries).

Anyways, this is my best shot at explaining this. I hope it can be at least somewhat enlightening.

like image 104
Mikael Persson Avatar answered Oct 15 '22 11:10

Mikael Persson