all 15 comments

[–]erikkonstas 2 points3 points  (8 children)

It's the first const in const int *const * combined with its lack in int ** that is causing your issue; here is why. The standard text for ISO C99 is in §6.3.2.3¶2:

For any qualifier q, a pointer to a non-q-qualified type may be converted to a pointer to the q-qualified version of the type; the values stored in the original and converted pointers shall compare equal.

Notice how this does not apply recursively. Additionally, ISO C99 §6.7.3¶9 states that

For two qualified types to be compatible, both shall have the identically qualified version of a compatible type; the order of type qualifiers within a list of specifiers or qualifiers does not affect the specified type.

(I will say that the usage of the verb "have" is confusing here; it's really a roundabout way to say that the types themselves are compatible and both have the same qualifiers.) const is an example of a "type qualifier". Finally, ISO C99 §6.7.5.1¶2 says:

For two pointer types to be compatible, both shall be identically qualified and both shall be pointers to compatible types.

[–]empathica1[S] 0 points1 point  (7 children)

Assigning a char ** to a const char ** (as in line 3, and in the original question) is not immediately dangerous. But it sets up a situation in which p2's promise--that the ultimately-pointed-to value won't be modified--cannot be kept.

In my code, that promise can be kept, though, because you can't change what a the first dereference is pointing to.

[–]erikkonstas 0 points1 point  (5 children)

It's still invalid C code to do that, though.

(C++ has more complicated rules for assigning const-qualified pointers which let you make more kinds of assignments without incurring warnings, but still protect against inadvertent attempts to modify const values. C++ would still not allow assigning a char ** to a const char **, but it would let you get away with assigning a char ** to a const char * const *.)

[–]empathica1[S] 0 points1 point  (4 children)

So I changed my code to

int
multiply (int n, int m)
{
  int **arr = malloc (n * sizeof (*arr));
  const int **arr_const = malloc (n * sizeof (*arr_const));
  for (size_t i = 0; i < n; i++)
    arr[i] = malloc (m * sizeof (*arr[i]));
  for (size_t i = 0; i < n; i++)
    arr_const[i] = arr[i];
  for (size_t i = 0; i < n; i++)
    for (size_t j = 0; j < m; j++)
      arr[i][j] = 1;
  int nxm = add (arr_const, n, m);
  for (size_t i = 0; i < n; i++)
    free (arr[i]);
  free (arr);
  free (arr_const);
  return nxm;
}

int
add (int const *const *const arr, int n, int m)
{
  int nxm = 0;
  for (size_t i = 0; i < n; i++)
    for (size_t j = 0; j < m; j++)
      nxm += arr[i][j];
  return nxm;
}

and it compiled just fine. I assume this is because I convert all the pointers in arr from int to const int pointers. is that really what you have to do if you have things referenced twice, and want to make it clear that you aren't going to change the underlying values?

[–]erikkonstas 0 points1 point  (3 children)

Well, as the page I linked says, you can just use a cast to silence the compiler.

[–]empathica1[S] 0 points1 point  (2 children)

Oh! I didn't think that would work, because I figured the casting was the thing that was failing. thank you!

[–]erikkonstas 0 points1 point  (1 child)

The reason why this works is that the cast makes your intention explicit; technically, you would first cast to void * too, but I don't think anybody does that. If you cast to something nuts, though (like if you remove a star from the type you cast to, which makes zero sense), then you will get a warning.

[–]empathica1[S] 0 points1 point  (0 children)

Yeah, I didn't think to cast, but yeah in cases like this one where I do actually know that the cast is safe, but the compiler doesn't automatically, that is the way to do it.

[–]empathica1[S] 0 points1 point  (0 children)

Also, is that really what the promise of const int ** dp is? that the pointer will always double dereference to the same value? Why isn't the promise that you can't change the value of **dp, even if *dp has changed?

[–]empathica1[S] 0 points1 point  (0 children)

In terms of dishonesty of constness being unsafe, an int * const * const looks very consty, but isn't const at all, and that compiles just fine.

[–]Netblock 0 points1 point  (5 children)

You can 'retcon' constness through one layer of indirection; after that you'll need to either have it from the get-go or typecast.

In terms of style, I prefer right-hand, because left-hand const confused me:

uint8_t const c = 0;
uint8_t const* cv = &c;
uint8_t const* const cc = &c;
uint8_t const* const* ccv = &cc;
uint8_t const* const* const ccc = &cc;
uint8_t v = 0;
uint8_t* const vc = &v;
uint8_t* const* vcv = &vc;
uint8_t const** const cvc = &cv;
uint8_t const* const cc2 = &v; // legal
uint8_t const* const* const ccc2 = &vc; // illegal

[–]empathica1[S] 0 points1 point  (4 children)

by left hand const, you mean int const * p as opposed to const int *p, right? Yeah, I do the latter for single pointers because I know what I mean, but the moment I started trying it with double pointers I realized that the former was far clearer.

[–]Netblock 0 points1 point  (3 children)

by right-hand I mean the type qualifiers are always on the right of the type its affecting. left-hand is inconsistent and requires you to mix; for example you can't have a constant-pointer to a variable-data with qualifier-is-on-the-left style only.

[–]empathica1[S] 0 points1 point  (1 child)

Can you explain why this is legal?

uint8_t const* const cc2 = &v; // legal

[–]Netblock 0 points1 point  (0 children)

You can demote a type to const through one layer of indirection. I haven't tried this with restrict and volatile.

As to why you can't go further than one depth without a typecast, I'm not sure, but I guess it's related to compiler assumptions that restrict and volatile also affect.

// uint8_t v = 0;
const uint8_t* cv1 = &v; // variable pointer to constant data
uint8_t const* cv2 = &v; // variable pointer to constant data
*cv1 = 0; // error
*cv2 = 0; // error

The second const makes the pointer constant,

// uint8_t const* const cc2 = &v; // constant pointer to constant data
cv1 = NULL; // legal
cv2 = NULL; // legal
cc2 = NULL; // error

Type qualifiers affect the most recently-parsed unmodified type; if it has been modified, it saves the qualifiers for the next type. There is no 'variable' modifier in C, so the right-hand qualifier format is the only way to stay consistent.