0

In C (let's say C11 if we need to specific), is the following program well-defined? Will it always print a=3 b=4 or could compiler optimizations affect the output?

(The real-world motivation is to provide a read-only public "view" of a struct that is only supposed to be modified by a particular module, i.e. source file.)

#include <stdio.h>
#include <stdlib.h>

struct obj_private {
    int a;
    int b;
};

struct obj_public {
    const int a;
    const int b;
};

int main(void) {
    void *mem = calloc(1, sizeof(struct obj_private));
    struct obj_private *priv = mem;
    struct obj_public *pub = mem;

    priv->a = 3;
    priv->b = 4;

    printf("a=%d b=%d\n", pub->a, pub->b);

    return 0;
}
John Kugelman
  • 349,597
  • 67
  • 533
  • 578
John Lindgren
  • 777
  • 5
  • 14
  • 4
    It would be better to just provide `const struct obj_private *`, it's effectively the same as what you're trying to achieve and it's well-defined. – Patrick Roberts Nov 22 '22 at 03:49
  • The structs are not compatible so it's not well-defined. If you really want to protect the private data the only way I know is to give users a copy of the private data. By sharing the address of the private data, you enable users to modify the private data. – Support Ukraine Nov 22 '22 at 07:40
  • The answers and comments so far are incomplete or incorrect—whether or not two types are compatible is only part of what determines whether an object may be inspected with a different type. If you want more information, update your question with more information about what you need to achieve and the circumstances for it. – Eric Postpischil Nov 22 '22 at 16:30
  • From my POV the basic question ("is the sample code well-defined") has been answered ("no it's not"). Thanks all. – John Lindgren Nov 22 '22 at 22:12
  • Duplicate: [What rules are there for qualifiers of effective type?](https://stackoverflow.com/q/65356861/584518) But I never received any satisfying answer. The C standard doesn't document it clearly, if at all. – Lundin Nov 25 '22 at 14:10

3 Answers3

2

These two types are not compatible. What you should be doing instead is using a pointer to a const object.

struct obj_private *priv = mem;
const struct obj_private *pub = mem;
dbush
  • 205,898
  • 23
  • 218
  • 273
  • Does this really protect the private data? Isn't it legal to do: `((struct obj_private *)pub)->a = 42;`, i.e. cast away contness and then modify the non const-qualified object ? – Support Ukraine Nov 22 '22 at 07:27
  • I don't believe this is correct. The effective type `int` is not compatible with `const int`, but `const int` _is_ a qualified version of a type compatible with the effective type of the object. Perfectly fine according to strict aliasing. – Lundin Nov 25 '22 at 14:18
-1

In the -fstrict-aliasing dialect of gcc, even structures with the same tag name, whose members are identical, are alias-incompatible if they are declared in different scopes. The only way two structures-type lvalues within a compilation unit will be treated as alias-compatible is if both are based upon the same struct declaration. Structures whose types are declared identically but separately are not alias compatible in the gcc dialect.

Consider, for example:

void test_write(void *p)
{
    struct foo { int x; };
    struct foo *pp = p;
    pp->x = 1;
}
struct foo { int x; };
int test_write_and_read(struct foo *p, int i, int j)
{
    p[i].x = 2;
    return p[j].x;
}
struct foo foo_arr[4];
int test(int i)
{
    test_write_and_read(foo_arr, 0, 1);
    test_write(foo_arr+i);
    return test_write_and_read(foo_arr, 1, 0);
}
int (*volatile vtest)(int) = test;
#include <stdio.h>
int main(void)
{
    int result = vtest(0);
    printf("%d %d\n", result, foo_arr[0].x);

}

In the gcc dialect, even though the type struct foo used within test_write has the same tag name and same members as the struct foo type used elsewhere, gcc will not allow for the possibility that a write to pp->x within test_write() might affect the value of foo_arr[j].x within test(), even though the address foo_arr+j is passed to that function.

Given that the gcc won't recognize any form of compatibility between the structures, despite their having the same tag name and identitical contents, one should not regard similarity between the contents of structures as an indication that gcc will accommodate the possibility of their being used interchangeably.

supercat
  • 77,689
  • 9
  • 166
  • 211
  • Do you mean `-fstrict-aliasing`, not `-fno-strict-aliasing`? Two structure types declared in different scopes in the same translation unit are incompatible regardless of whether `-fstrict-aliasing`, `-fno-strict-aliasing`, or neither is specified. `-fstrict-aliasing` affects whether the compiler supports aliasing one type as another, not whether they are compatible. – Eric Postpischil Nov 25 '22 at 01:46
  • @EricPostpischil: I use the phrase "alias-compatible" to refer types which may be used to access the same storage, at least if the pointers are converted through `void`. I don't think the authors of the Standard intended nor expected that compiler writers would use the Standard as an excuse to blindly ignore the possibility that the call `test_write(foo_arr+j)` might actually affect the value of `foo_arr[j].x`, but that's what gcc does. Testing clang in this particular scenario doesn't show the same assumption, but I don't know whether clang seeks to reliably handle such constructs. – supercat Nov 26 '22 at 07:12
  • Re “In the `-fno-strict-aliasing` dialect of gcc”: With `-fno-strict-aliasing`, GCC treats the structures as if they may alias, [producing the expected output of “1 1”](https://godbolt.org/z/P6WGoWeKM). – Eric Postpischil Nov 26 '22 at 13:09
  • @EricPostpischil: Corrected. The `-fstrict-aliasing dialect` fails to recognize the possibility that the lvalues might identify the same storage (tested on godbolt). I had the command-line switch backward. My point was that since even structures with identical contents aren't treated as alias-compatible, the question of whether a difference in the const qualifiers of members would be sufficient to make structures incompatible is moot. – supercat Nov 26 '22 at 17:28
-1
  • void *mem = calloc(1, sizeof(struct obj_private)); Here mem points at a chunk of memory with no effective type and no declared type.
  • priv->a = 3; Here one of the locations a gets effective type int 1).
  • int since the lvalue expression priv->a used for the access is of type int 2). Same with b.
  • pub->a Here the effective type is lvalue accessed as const int. This is well-defined behavior 3). The effective type is int. The lvalue expression pub->a has type const int - it is not a type compatible with the effective type of the object but it is a qualified version of such a type.

Thus well-defined behavior, the C standard is pretty clear. If these rules make sense to begin with or if the intention of the Committee was to allow this kind of type punning, well that's another story.


1) C17 6.5/6

If a value is stored into an object having no declared type through an lvalue having a type that is not a character type, then the type of the lvalue becomes the effective type of the object for that access and for subsequent accesses that do not modify the stored value.

2) C17 6.5/6

For all other accesses to an object having no declared type , the effective type of the object is simply the type of the lvalue used for the access.

3) C17 6.5.7

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:

  • a type compatible with the effective type of the object,
  • a qualified version of a type compatible with the effective type of the object,
Lundin
  • 195,001
  • 40
  • 254
  • 396
  • (a) Based on purpose: A principle reason for the aliasing rules is so that the compiler may assume a function declared `void foo(struct foo *p, struct bar *q)` is passed pointers to discrete objects; they do not overlap. E.g., we might have `struct Complex { float re, im; }` and `struct Point { float x, y; }` with different meanings to the program and to the compiler even though they have identical members. If permissible aliasing depended only on the member types and not the structure types, this would break. – Eric Postpischil Nov 25 '22 at 14:57
  • (b) Based on rules: `priv->a` and `pub->a` are lvalues with `int` type, so that is fine. However, `priv->a` and `pub->a` are effectively `(*priv).a` and `(*pub).a`; they (conceptually) access the structure before they get the member for it. So it is not just the `int` types of `priv->a` and `pub->a` that must satisfy the aliasing rules; it is the structure types as well. – Eric Postpischil Nov 25 '22 at 14:59
  • @EricPostpischil I didn't write the C standard. It clearly allows lvalue access using a qualified type compatible with the effective type, the text is as unambiguous as can be. As for your structure reasoning, C17 6.5/7 "an aggregate or union type that includes one of the aforementioned types among its members" The aforementioned type being either a compatible type or a qualified type compatible with the effective type. Again, this is unambiguous. It might very well be a very poorly written part of the standard, but don't blame me for that. – Lundin Nov 25 '22 at 15:09
  • There are two lvalues in `pub->a`, the access to the `struct obj_public` and the access to its member `a`. When you say “It clearly allows lvalue access using a qualified type compatible with the effective type”, that explains why the access to the member would, by itself, be defined. It does not explain why access to the structure is defined. Regarding “an aggregate or union type that includes one of the aforementioned types among its members”, this also does not explain why access to the structure would be allowed, as `struct obj_private` does not have `struct obj_public` among its members. – Eric Postpischil Nov 25 '22 at 15:30