3

In a C module (aka, compilation unit), I want to have some private data, but expose it read-only to the outside world. I achieve that by having a field in a struct declared in my .c file and a function declared in my .h file that returns a pointer to const to that field. For example, this could look like the following for a string:

// header:

typdef struct foo foo;

const char *foostr(const foo *f);

// implementation:

struct foo
{
    char *str;
};

const char *foostr(const foo *f)
{
    return foo->str;
}

Now my problem is, I have an array of objects that are themselves arrays. So in my struct, I have a pointer to an array, and from my function, I try to return a pointer to the corresponding const array. Consider the following example code:

#include <stdlib.h>
#include <stdint.h>

typedef uint8_t shape[64];

typedef struct foo
{
    shape *shapes;
} foo;


foo *createfoo(void)
{
    foo *f = malloc(sizeof *f);
    if (!f) return 0;

    // 10 empty shapes:
    f->shapes = calloc(10, sizeof *(f->shapes));

    if (!f->shapes)
    {
        free(f);
        return 0;
    }

    return f;
}

const shape *fooshapes(const foo *f)
{
    return f->shapes;
}

Compiling this with gcc -c -std=c11 -Wall -Wextra -pedantic, I get the following warning:

constarr.c: In function ‘fooshapes’:
constarr.c:31:13: warning: pointers to arrays with different qualifiers are incompatible in ISO C [-Wpedantic]
     return f->shapes;
            ~^~~~~~~~

I understand a double pointer isn't compatible to a double pointer to const, and also the reason for it, but I don't think this is related, or is it? So, why isn't it allowed to implicitly convert a pointer to an array to a pointer to a const array? And any ideas what I should do instead?

What I did now is adding an explicit cast like this:

const shape *fooshapes(const foo *f)
{
    return (const shape *) f->shapes;
}

This of course silences the compiler and I am almost sure it will always work correctly in practice. The "const hole" can't exist in this case, as with an array, there is no non-const inner pointer. But it still leaves me with two further questions:

  • Is my assumption correct that this doesn't lead to a hole in const correctness?
  • Does the explicit cast violate the standard here?
  • 1
    The cast isn't automatic, but you can do it. Making pointers `const` does not make anything read-only. Whoever is calling your function can just cast it to non-const and modify the contents at will. – FBergo May 03 '18 at 22:56
  • @FBergo a cast to non-const is doable, still forbidden (undefined results). I don't try to guard against wrong usage of C here, this is of course impossible.. –  May 03 '18 at 22:58
  • Well, clang doesn't complain: https://godbolt.org/g/82j7rm ... *Maybe* related: https://stackoverflow.com/a/8909208/4944425 – Bob__ May 03 '18 at 23:34
  • @Bob__ thanks, both is very interesting :) So the solution for me is "*switch to clang*" ;) –  May 04 '18 at 05:13
  • Same question in different syntax, https://stackoverflow.com/questions/28062095/ – M.M Nov 21 '19 at 02:00

4 Answers4

2

It is indeed the same double pointer issue you refer to in the question.

You can convert pointer-to-T to pointer-to-const-T. But const applied to an array qualifies the element type, not the array type (C11 6.7.3.9), so that's not what you're trying to do here. You're not trying to convert pointer-to-array-of-T to pointer-to-const-array-of T, but rather trying to convert to pointer-to-array-of-const-T, and those two types are not compatible in C.

Crowman
  • 25,242
  • 5
  • 48
  • 56
  • Thanks for your answer! I assumed something like that, but **§6.7.3 p9** doesn't tell me the whole thing. The array itself is "kind of const" anyways, so the loophole like with a double pointer wouldn't exist here. "*those two types are not compatible in C.*" <- just because they're not listed in "compatible types" or do you know whether there's a reason given somewhere? Meanwhile, the [answer linked by Bob__](https://stackoverflow.com/questions/50164803/pointer-to-array-not-compatible-to-a-pointer-to-const-array/50165840#comment87348166_50164803) seems interesting. ... –  May 04 '18 at 05:10
  • I extended my question a bit, sorry for not writing more in the first place -- it shouldn't invalidate your answer, just letting you know. –  May 04 '18 at 07:11
2

The issue is that by typedef:ing an array and then const-qualifying a pointer to that type, you actually get an const uint8_t(*)[64], which is not compatible with uint8_t(*)[64] 1). Const correctness and array pointers behave awkwardly together, see this for an example of the same issue.

Anyway, the root of the problem in this specific case is hiding an array behind a typedef. This is usually not a good idea. You can fix this by wrapping the array inside a struct instead, which might also give a better design overall. Example:

typedef struct shape
{
  uint8_t shape[64];
} shape_t;

typedef struct foo
{
  shape_t shapes;
} foo_t;

Now you can return a const shape_t* just fine.

Optionally you can now either make shape_t an opaque type just like foo_t. Or you can make the internals of shape_t public by for example exposing the struct declaration in a public header shape.h.


1) Implicit conversion between a pointer-to-type and a qualified-pointer-to-type is the only allowed implicit conversion.

C11 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.

This does not apply here. For the conversion to be ok, it would have to be a conversion from pointer-to-array-type to pointer-to-qualified-array-type.

But it is not, it is a conversion from pointer to-array-type to qualified-pointer-to-array-type.

Normative text for compatible types in C is chapter 6.2.7, which only references further to 6.7.3. Relevant parts:

C11 6.7.3/9

If the specification of an array type includes any type qualifiers, the element type is so-qualified, not the array type.

and C11 6.7.3/10

For two qualified types to be compatible, both shall have the identically qualified version of a compatible type

This is why gcc correctly issues a diagnostic message - the pointers are not identically qualified versions.

Lundin
  • 195,001
  • 40
  • 254
  • 396
  • @Stargateur Yes, re-writing the code by removing the (pointer to) array is my proposed solution indeed. – Lundin May 04 '18 at 08:51
  • As a side-note, using an array pointer cast should work too. That was the solution that several people came up with in the linked question and I don't see how it would violate the standard or cause any aliasing issues. It's just that returning array pointers from functions is a rather icky thing to do in the first place. – Lundin May 04 '18 at 08:54
  • Example: `#define const_cast(n, arr) _Generic(arr, int(*)[n] : (const int(*)[n])arr ) `. – Lundin May 04 '18 at 08:56
  • It's a ["critical undefined behavior"](https://port70.net/~nsz/c/c11/n1570.html#L.3p2) ;) – Stargateur May 04 '18 at 09:00
  • @Stargateur No, since the effective type of the original object is not a const qualified type. "Critical undefined behavior" would be to cast to const first, then later cast away the const again. – Lundin May 04 '18 at 09:03
  • No you misinterpreted it, this is not a const problem, it's a problem that nothing disallow `const double *` to not be the same size or even the same value of `double *`, you are not allow to change a nested qualifier pointer because of it. – Stargateur May 04 '18 at 09:04
  • @Stargateur The only issue the C standard has with casting from array pointer to qualified array pointer is 6.3.2.3/7. That is, alignment etc. As for your example here, it is not correct (and not the same problem as in the question), since 6.3.2.3/2 requires that a `double*` compares equal to a `const double*`. – Lundin May 04 '18 at 09:07
  • **Compare equal doesn't mean the same value.** I really don't like talk to you when it's come to C strictly standard question, you are argue that these type are compatible despite the fact compile write very clearly "incompatible type". And yes, this is entirely related to this question/problem ! – Stargateur May 04 '18 at 09:10
  • @Stargateur Because `const uint8_t(*)[64]` versus `uint8_t(*)[64]` is not the same thing as `const double*` versus `double*`. The latter (pointer to qualified data) is 100% covered by 6.3.2.3/2. The former is not mentioned explicitly, because it is a qualified pointer to data, which is not the same thing as pointer to qualified data. – Lundin May 04 '18 at 09:16
  • 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.` – Lundin May 04 '18 at 09:16
  • Thanks for this answer! Although it doesn't completely clarify the situation for me (e.g., **why** are these types incompatible? The reason given for double pointers doesn't exist here ... and is casting in this case ok as well?) -- it gives a very valuable idea for a "workaround", therefore have an upvote and a thanks :) Wrapping my object in a "struct" just to get around a strange limitation of C doesn't feel perfect to me, but it would be a possible way. Minor nitpick, I wouldn't call a type `_t` because of POSIX reserving that namespace ... –  May 04 '18 at 09:18
  • You mix up everything I said, this doesn't make sense to continue the discussion. – Stargateur May 04 '18 at 09:19
  • I have added references to the standard to the answer, explaining why there is a warning from gcc. – Lundin May 04 '18 at 09:29
  • @Lundin thanks for that as well. By **why**, I meant the motivation behind (like, there is a clear example for the case of a double pointer where an implicit conversion from `T **` to `const T **` would punch a hole in the const guarantee). I'm missing such a thing for this array case, but now assume it just doesn't exist and this case wasn't thoroughly thought of in the C spec :o –  May 04 '18 at 09:44
  • @Lundin "*As a side-note, using an array pointer cast should work too.*" I'm pretty sure about that as well. But would it violate the standard? If yes, I'll change my code to use the "struct workaround" :) –  May 04 '18 at 09:51
  • 1
    @FelixPalmen No it wouldn't, as soon as you use an explicit cast, the only part of the standard that applies is 6.3.2.3/7, which is only concerned about fundamental stuff like alignment and the actual data representation. And there should be no strict aliasing issues either, since the data is really an array of uint8_t. – Lundin May 04 '18 at 09:58
1

pointer to array not compatible to a pointer to 'const' array?

Yes they are incompatible, you can't change a nested const qualifier, int (*a)[42] is not compatible with int const (*b)[42] related, as double ** is not compatible with double const **

Is my assumption correct that this doesn't lead to a hole in const correctness?

Well, you are adding const qualifier so no. In fact, return non const don't produce a warning and don't break const correctness according to the same rule that doesn't allow you to add const. But code like that are very confuse but are not undefined behavior.

shape *fooshapes(const foo *f)
{
    return f->shapes;
}

Does the explicit cast violate the standard here?

Strictly, Yes, as compiler said there types are incompatibles, however I don't think it would produce a bug in any actual classic hardware.

The thing is the standard don't guarantee that double *a and double const *b have the same size or even the same value but just guarantee that a == b will produce a positive value. You can promote any double * to double const * but like I said you must promote it. When you cast you don't promote anything because the array/pointer is nested. So it's undefined behavior.

It's a "you simply can't do that in C", you could ask but "what I'm suppose to do ?". Well, I don't see the purpose of your structure neither of your pointer to array in the structure. To fix the deeper problem of your data structure, you should ask an other question where you talk more about what is your purpose with this code. For example, your function could do something like that:

uint8_t const *fooshapes(const foo *f, size_t i)
{
    return f->shapes[i];
}
Stargateur
  • 24,473
  • 8
  • 65
  • 91
  • "*Well, I don't see the purpose of your structure neither of your pointer to array in the structure.*" I explained the purpose in my introduction to the question. Of course, the code shown is not the real code, it's stripped down as much as possible. The link you give sheds some more light, thanks, this really seems to be a shortcoming in the C specification (while C++ does "the right thing"). –  May 04 '18 at 09:41
  • @FelixPalmen C++ loose some flexibility to do "the right thing", this is a choice, "I want to have some private data, but expose it read-only to the outside world." doesn't explain what do you want to do with this code. There is a design flow in your MCVE, that why I ask you that. It's impossible to help you to find a good solution without know what is the real purpose of this data structure. Fixing your MCVE, would be as easy as remove all the code, as it does nothing. – Stargateur May 04 '18 at 09:44
  • Others obviously understood the purpose quite well. And the workaround suggested by Lundin will actually serve that purpose ... I'm not really happy with it, but it looks like it's the best option, giving C doesn't seem to support what I need here. The code shown isn't supposed to **do** something, but to **expose** some private data read-only. –  May 04 '18 at 09:48
  • "*When you cast you don't promote anything because the array/pointer is nested*" I have doubts about that reasoning. With a *pointer to array*, there is no nested pointer... btw, didn't you mean *convert*? –  May 04 '18 at 09:55
  • @FelixPalmen Yes, I think I understand quite well C code so I understand quite well your primary purpose. But you clearly didn't understand my last exemple, if your purpose is to expose some private data, I suppose you want to iterate over it so you should do something similar to the snipped I give. Other answer that tell you to cast it are easy to accept but this doesn't make your code good. I repeat it a last time, what is the use case of this function with this kind of data, I bet that you are wrong somewhere. promote/convert are similar and yes I'm sure of what I said. – Stargateur May 04 '18 at 09:55
  • Now I finally get what you're aiming for. With the assumption that the calling code wants to iterate over this array, your suggestion makes sense, therefore have an upvote :) unfortunately, explaining my whole project here would far exceed the scope of this question ... the calling code won't iterate but use some `memcpy()`s on the result. –  May 04 '18 at 10:08
  • @FelixPalmen "exceed the scope of this question" yes of course. "the calling code won't iterate but use some memcpy()s on the result." maybe `fooshapes()` could do it itself ? Like this `fooshapes()` would not expose his internal data. – Stargateur May 04 '18 at 10:15
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/170355/discussion-between-felix-palmen-and-stargateur). –  May 04 '18 at 10:17
0

I am not entirely sure if this could be considered a good idea, but something that I do sometimes when I'm feeling lazy is to define a "const cast" macro like:

#define CC(_VAR) ((const typeof(_VAR))(_VAR))

This of course assumes that you have a compiler that supports the typeof extension. Using this (dubious) construction you could then write

const shape *fooshapes(const foo *f)
{
    return CC(f->shapes);
}

Otherwise, C doesn't generally cast to const implicitly. You do have to explicitly cast pointers to const pointers.

Roflcopter4
  • 679
  • 6
  • 16
  • Thanks for your answer! I should have mentioned that explicit casting of course is an option, but I try to avoid that -- it mostly serves to silence compiler warnings. Your last sentence isn't correct though ... a conversion from `T *` to `const T *` is implicit in C, therefore I was surprised this is different when `T` is an array type. –  May 03 '18 at 23:11
  • 1
    @FelixPalmen I think you're correct there. I have noticed this inconsistency myself, hence the silly macro. I just assumed C didn't cast pointers and GCC's warnings were a bit buggy. Perhaps it's only array type pointers? – Roflcopter4 May 04 '18 at 00:30
  • I extended my question a bit, sorry for not writing more in the first place -- it shouldn't invalidate your answer, just letting you know. –  May 04 '18 at 07:11
  • 1
    `typeof` is not standard C, use _Generic instead. – Lundin May 04 '18 at 08:31