38

I stumbled upon a small issue while trying to make const-correct code.

I would have liked to write a function that takes a pointer to a const struct, to tell to the compiler "please tell me if I am modifying the struct, because I really do not want to".

It suddenly came to my mind that the compiler will allow me to do this:

struct A
{
    char *ptrChar;
};

void f(const struct A *ptrA)
{
    ptrA->ptrChar[0] = 'A'; // NOT DESIRED!!
}

Which is understandable, because what actually is const is the pointer itself, but not the type it points to. I would like for the compiler to tell me that I'm doing something I do not want to do, though, if that is even possible.

I used gcc as my compiler. Although I know that the code above should be legal, I still checked if it would issue a warning anyways, but nothing came. My command line was:

gcc -std=c99 -Wall -Wextra -pedantic test.c

Is it possible to get around this issue?

jscs
  • 63,694
  • 13
  • 151
  • 195
Samuel Navarro Lou
  • 1,168
  • 6
  • 17
  • this case work to assignment of member. E.g `ptrA->ptrChar = malloc(2);` Which member is pointing is not so. – BLUEPIXY Feb 26 '16 at 08:11
  • [Similar question](http://stackoverflow.com/questions/13181546/const-correctness-for-structs-with-pointers) – M.M Feb 26 '16 at 08:52
  • For this purpose I usually have a header that just forward declares `struct a;`, and some function declarations as in `a_do_something(const struct *a);` Including the whole definition of `struct A` in a header file is rarely necessary. You put a lot of functions like your `f` in separate translation units, where `f` would only use ptrA somehow like this `stuff = a_do_something(ptrA);` – Gábor Buella Mar 06 '16 at 13:26

7 Answers7

20

A way to design your way around this, if needed, is to use two different types for the same object: one read/write type and one read-only type.

typedef struct
{
  char *ptrChar;
} A_rw;

typedef struct
{
  const char* ptrChar;
} A_ro;


typedef union
{
  A_rw rw;
  A_ro ro;  
} A;

If a function needs to modify the object, it takes the read-write type as parameter, otherwise it takes the read-only type.

void modify (A_rw* a)
{
  a->ptrChar[0] = 'A';
}

void print (const A_ro* a)
{
  puts(a->ptrChar);
}

To pretty up the caller interface and make it consistent, you can use wrapper functions as the public interface to your ADT:

inline void A_modify (A* a)
{
  modify(&a->rw);
}

inline void A_print (const A* a)
{
  print(&a->ro);
}

With this method, A can now be implemented as opaque type, to hide the implementation for the caller.

Peter - Reinstate Monica
  • 15,048
  • 4
  • 37
  • 62
Lundin
  • 195,001
  • 40
  • 254
  • 396
  • 1
    I like the union and am fairly sure that it will work in practice; is it allowed? The struct types ar, iiuc, incompatible. – Peter - Reinstate Monica Feb 26 '16 at 09:00
  • @PeterA.Schneider I believe so, as per the "strict aliasing rule" (6.5/7) and the union "type punning rule" (6.2.6.1/7). Though according to the latter, this might be unspecified behavior. It doesn't really matter, because like you say, this will always work in practice. I don't see how it would make sense for the compiler to implement this code any differently than expected, no matter any subtle rules in the standard. It might not work in C++ however, because iirc C++ doesn't allow type punning through unions. – Lundin Feb 26 '16 at 09:23
  • 5
    @Lundin: Beware gcc. It is quite aggressive about optimizing based on assumptions that it can derive from the strict aliasing rule. – Martin Bonner supports Monica Feb 26 '16 at 10:21
  • Also the way I thought how do it, nicely not using macros but plain C. Nice clean code! – ljrk Feb 26 '16 at 11:21
  • @MartinBonner I believe strict aliasing shouldn't be a problem. `"An object shall have its stored value accessed only by an lvalue expression that has one of the following types:"` /--/ `"a qualified version of a type compatible with the effective type of the object"` /--/ `"an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union)"` If I understand the text inside the parenthesis correctly, it seems to state that my code is fine. – Lundin Feb 26 '16 at 12:25
  • 1
    Yes. I think you are right. What worried me was the "it should work in practise" - unless the standard says it must, it's quite likely not to (occasionally). – Martin Bonner supports Monica Feb 26 '16 at 14:09
  • 1
    @MartinBonner What I mean with that comment is, first you give the compiler two structs inside a union, each containing a compatible pointer type. There is just no way it can allocate each struct differently, why would it? Each struct _will_ have the same memory layout. And when you store an address in either of the structs, it will be stored exactly the same, no matter the const qualifier. Common sense dictates that this is how the data type must work and no other implementation makes sense. At that point, the standard turns irrelevant, because nothing it says can change reality. – Lundin Feb 26 '16 at 14:29
  • The problem comes when the compiler spots that you never write through the const pointer so it can cache the first read, and ignore all the writes through the non-const pointer. The section of the standard you quoted means it can't do that in this particular case. – Martin Bonner supports Monica Feb 26 '16 at 18:20
  • 2
    You say "If a function needs to modify the object, it takes the read-only type as parameter, otherwise it takes the read-write type". I assume this is a typo and those roles are reversed. – Chris Hayes Feb 26 '16 at 18:56
  • @ChrisHayes I fixed that. It's fairly clear and I believe Lundin is fine with that. – Peter - Reinstate Monica Mar 01 '16 at 23:08
7

This is an example of implementation vs. interface, or "information hiding" -- or rather non-hiding ;-) -- issue. In C++ one would simply have the pointer private and define suitable public const accessors. Or one would define an abstract class -- an "interface" -- with the accessor. The struct proper would implement that. Users who do not need to create struct instances would only need to see the interface.

In C one could emulate that by defining a function which takes a pointer to the struct as parameter and returns a pointer to const char. For users who do not create instances of these structs, one could even provide a "user header" which does not leak the struct's implementation but only defines manipulating functions taking (or returning, like a factory) pointers. This leaves the struct an incomplete type (so that only pointers to instances could be used). This pattern effectively emulates what C++ does behind the scenes with the this pointer.

Peter - Reinstate Monica
  • 15,048
  • 4
  • 37
  • 62
6

Maybe if you decide to use C11 you can implement a Generic macro which refers either to the constant or variable version of the same member (you should also include a union in your structure). Something like this:

struct A
{
    union {
        char *m_ptrChar;

        const char *m_cptrChar;
    } ;
};

#define ptrChar_m(a) _Generic(a, struct A *: a->m_ptrChar,        \
                                 const struct A *: a->m_cptrChar)//,  \
                                 //struct A: a.m_ptrChar,        \
                                 //const struct A: a.m_cptrChar)

void f(const struct A *ptrA)
{
    ptrChar_m(ptrA) = 'A'; // NOT DESIRED!!
}

The union creates 2 interpretation for a single member. The m_cptrChar is a pointer to constant char and the m_ptrChar to non-constant. Then the macro decides which to refer depending on the type of it's parameter.

The only problem is that the macro ptrChar_m can work only with either a pointer or object of this structure and not the both.

AnArrayOfFunctions
  • 3,452
  • 2
  • 29
  • 66
  • 1
    Nice solution, though you have to drop the `.` cases, or you'll get problems with macro expansion. Also avoid variable names starting with underscores (see 7.1.3). – Lundin Feb 26 '16 at 08:50
  • 4
    Please don't do this. That's horrible. Nobody is going to understand this in a year, not even you. – fuz Feb 26 '16 at 10:08
  • I don't like the union punning. Could we instead use `_Generic(a, struct A*: a->ptrChar, const struct A*: (const char *) a->ptrChar )` ? In that way, we don't have to change the definition of the struct. – chi Feb 26 '16 at 10:24
  • @FUZxxl That's the point. Complex working code is my favorite. Though this solution is maybe really not the best (my personal opinion). – AnArrayOfFunctions Feb 26 '16 at 17:42
5

This is a known issue of the C language and not avoidable. After all, you are not modifying the structure, you are modifying a separate object through a non const-qualified pointer that you obtained from the structure. const semantic was originally designed around the need to mark memory regions as constant that physically aren't writable, not around any concerns for defensive programming.

fuz
  • 88,405
  • 25
  • 200
  • 352
3

We could hide the information behind some "accessor" functions:

// header
struct A;           // incomplete type
char *getPtr(struct A *);
const char *getCPtr(const struct A *);

// implementation
struct A { char *ptr };
char *getPtr(struct A *a) { return a->ptr; }
const char *getCPtr(const struct A *a) { return a->ptr; }
chi
  • 111,837
  • 3
  • 133
  • 218
2

No, unless you change the struct definition to:

struct A
{
    const char *ptrChar;
};

Another convoluted solution that keeps the old struct definition intact, is to define a new struct with identical members, whose relevant pointer members are set to: points to const type. Then the function you are calling is changed to take the new struct. A wrapper function is defined that takes the old struct, does a member by member copy to the new struct and passes it to the function.

2501
  • 25,460
  • 4
  • 47
  • 87
1

Can GCC warn me about modifying the fields of a const struct in C99?

You are not modifying the fields of a const struct.

A value of struct A contains a pointer to a non-const char. ptrA is a pointer to a const struct A. So you can't change the struct A value at *ptrA. So you can't change the pointer to char at (*ptrA).Char aka ptrA->ptrChar. But you are changing the value at where ptrA->ptrChar points, ie the value at *(ptrA->Char) aka ptrA->Char[0]. The only consts here are struct As and you're not changing a struct A so what exacty is "not desired"?

If you don't want to allow change to the value at where a struct A's Char field points (via that struct A) then use

struct A
{
    const char *ptrChar; // or char const *ptrChar;
};

But maybe what you think you are doing in f is something like

void f(const struct A *ptrA)
{
    const char c = 'A';
    ptrA->ptrChar = &c;
}

Which will get an error from the compiler.

philipxy
  • 14,867
  • 6
  • 39
  • 83