Is it a strict aliasing violation to alias a struct as its first member?
Asked Answered
D

3

25

Sample code:

struct S { int x; };

int func()
{
     S s{2};
     return (int &)s;    // Equivalent to *reinterpret_cast<int *>(&s)
}

I believe this is common and considered acceptable. The standard does guarantee that there is no initial padding in the struct. However this case is not listed in the strict aliasing rule (C++17 [basic.lval]/11):

If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined:

  • (11.1) the dynamic type of the object,
  • (11.2) a cv-qualified version of the dynamic type of the object,
  • (11.3) a type similar (as defined in 7.5) to the dynamic type of the object,
  • (11.4) a type that is the signed or unsigned type corresponding to the dynamic type of the object,
  • (11.5) a type that is the signed or unsigned type corresponding to a cv-qualified version of the dynamic type of the object,
  • (11.6) an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
  • (11.7) a type that is a (possibly cv-qualified) base class type of the dynamic type of the object,
  • (11.8) a char, unsigned char, or std::byte type.

It seems clear that the object s is having its stored value accessed.

The types listed in the bullet points are the type of the glvalue doing the access, not the type of the object being accessed. In this code the glvalue type is int which is not an aggregate or union type, ruling out 11.6.

My question is: Is this code correct, and if so, under which of the above bullet points is it allowed?

Demote answered 17/5, 2018 at 4:6 Comment(2)
I'm mainly familiar with the C Standard rather than the C++ Standard, but the authors of the former didn't think it necessary to specify any situations where an lvalue of an aggregate member type can actually be used to access the aggregate. Even something like myStruct.member=23; invokes UB unless member has a character type, but a compiler would have to be rather obtuse not to recognize such usage. A compiler would likewise have to be obtuse not to recognize situations where a pointer which is freshly converted to a member type is used to access that member. The Standard, however...Naturalist
...does not mandate such behavior, but relies upon compiler writers recognizing that quality implementations should behave usefully even in cases not mandated by the Standard. Unfortunately, such reliance turns out to have been misplaced.Naturalist
D
19

The behaviour of the cast comes down to [expr.static.cast]/13;

A prvalue of type “pointer to cv1 void” can be converted to a prvalue of type “pointer to cv2 T”, where T is an object type and cv2 is the same cv-qualification as, or greater cv-qualification than, cv1. If the original pointer value represents the address A of a byte in memory and A does not satisfy the alignment requirement of T , then the resulting pointer value is unspecified. Otherwise, if the original pointer value points to an object a, and there is an object b of type T (ignoring cv-qualification) that is pointer-interconvertible with a, the result is a pointer to b. Otherwise, the pointer value is unchanged by the conversion.

The definition of pointer-interconvertible is:

Two objects a and b are pointer-interconvertible if:

  • they are the same object, or
  • one is a union object and the other is a non-static data member of that object, or
  • one is a standard-layout class object and the other is the first non-static data member of that object, or, if the object has no non-static data members, the first base class subobject of that object, or
  • there exists an object c such that a and c are pointer-interconvertible, and c and b are pointer-interconvertible.

So in the original code, s and s.x are pointer-interconvertible and it follows that (int &)s actually designates s.x.

So, in the strict aliasing rule, the object whose stored value is being accessed is s.x and not s and so there is no problem, the code is correct.

Demote answered 17/5, 2018 at 4:28 Comment(6)
But you're not doing a static_cast and there's no "pointer to cv void" here? I don't see how this is relevant.Frankish
Basically, scratch the static.cast ref and include the very important sentence after the block you cited in basic.compoundFrankish
@Frankish reinterpret_cast between unrelated pointer types is defined as a sequence of two static_casts via void *Demote
Note the result is a pointer to b means the object is b,hence the pointer of (int*)&s is to x of type int,so the dereference of (int*)&s is the x within object s,it's dynamic type is int,so access the value by glvaue int& is legalAuxesis
@Demote your analyzation is excatly perfert for the case of pointer, but I also find a sentence in [expr.reinterpret.cast], that is: A glvalue expression of type T1 can be cast to the type “reference to T2” if an expression of type “pointer to T1” can be explicitly converted to the type “pointer to T2” using a reinterpret_­cast. The result refers to the same object as the source glvalue, but with the specified type. Please notice the emphasized part. It seems to say that the result still refer to the object of type S in your example since the source glvalue is s.Auxesis
@Auxesis good observation; this was an issue in the standard and the wording was changed: CWG Issue 2342. The answer is correct for references as well as pointersPenthea
A
6

I think it's in expr.reinterpret.cast#11

A glvalue expression of type T1, designating an object x, can be cast to the type “reference to T2” if an expression of type “pointer to T1” can be explicitly converted to the type “pointer to T2” using a reinterpret_­cast. The result is that of *reinterpret_­cast<T2 *>(p) where p is a pointer to x of type “pointer to T1”. No temporary is created, no copy is made, and no constructors or conversion functions are called [1].

[1] This is sometimes referred to as a type pun when the result refers to the same object as the source glvalue

Supporting @M.M's answer about pointer-incovertible:

from cppreference:

Assuming that alignment requirements are met, a reinterpret_cast does not change the value of a pointer outside of a few limited cases dealing with pointer-interconvertible objects:

struct S { int a; } s;


int* p = reinterpret_cast<int*>(&s); // value of p is "pointer to s.a" because s.a
                                     // and s are pointer-interconvertible
*p = 2; // s.a is also 2

versus

struct S { int a; };

S s{2};
int i = (int &)s;    // Equivalent to *reinterpret_cast<int *>(&s)
                     // i doesn't change S.a;
Antin answered 17/5, 2018 at 4:11 Comment(3)
That text seems to just be defining that (int &)s means *reinterpret_cast<int *>(&s), but it doesn't explain any further about how the latter interacts with the strict aliasing ruleDemote
You did point me in the right direction to finding what I believe is the correct answer though; thank youDemote
@M.M, you're right. thanks too for providing the pointer-inconvertible explanation. I've added a reference too supporting your claim.Antin
N
1

The cited rule is derived from a similar rule in C89 which would be nonsensical as written unless one stretches the meaning of the word "by", or recognizes what "Undefined Behavior" meant when C89 was written. Given something like struct S {unsigned dat[10];}s;, the statement s.dat[1]++; would clearly modify the stored value of s, but the only lvalue of type struct S in that expression is used solely for the purpose of producing a value of type unsigned*. The only lvalue which is used to modify any object is of type int.

As I see it, there are two related ways of resolving this issue: (1) recognizing that the authors of the Standard wanted to allow cases where an lvalue of one type was visibly derived from one of another type, but didn't want to get hung up on details of what forms of visible derivation must be accounted for, especially since the range of cases compilers would need to recognize would vary considerably based upon the styles of optimization they performed and the tasks for which they were being used; (2) recognizing that the authors of the Standard had no reason to think it should matter whether the Standard actually required that a particular construct be processed usefully, if it would be have been clear to everyone that there was reason to do otherwise.

I don't think there has consensus among the Committee members over whether a compiler given something like:

struct foo {int ct; int *dat;} it;
void test(void)
{
  for (int i=0; i < it.ct; i++)
    it.dat[i] = 0;
}

should be required to ensure that e.g. after it.ct = 1234; it.dat = &it.ct;, a call to test(); would zero it.ct and have no other effect. Parts of the Rationale would suggest that at least some committee members would have expected so, but the omission of any rule that would allow for an object of structure type to be accessed using an arbitrary lvalue of member type suggests otherwise. The C Standard has never really resolved this issue, and the C++ Standard cleans things up somewhat but doesn't really solve it either.

Naturalist answered 16/11, 2019 at 21:44 Comment(12)
The code in your example seems clearly legal: we are accessing the stored value of an object of type int (namely it.ct) through an lvalue of type int (namely it.dat[i]).Hadik
It seems to me that the issue arises instead through code like the following: struct bar { int x,y; } b1, b2; int *p; void test2(void) { p = &b2.y; *p = 17; b1 = b2; }. If we are not allowed to access b2 using an object of type int, then this would suggest that the assignment b1 = b2 could be moved up, and we might not end up with b1.y == 17. But that logic would also suggest that b2.y = 17; all by itself is undefined behavior, which is absurd.Hadik
@NateEldredge: When the Standard was written, the phrase "Undefined Behavior" was intended to, among other things, "identify areas of conforming language extension". The Committee thus didn't make any real effort to avoid characterizing as UB actions which they expected that implementations would generally process identically. I think it's clear that your example should work predictably, since p is used when it's freshly visibly derived from b2, but if one were to write something like e.g. int test2(struct bar *p1, int *p2) { p1->x=1; *p2=3; return p1->x; }...Naturalist
...without any visible relationship between p2 and struct bar, I don't think it's clear that compilers should be required to allow for the possibility of p2 aliasing bar->x. In the vast majority of cases where code uses a pointer to access a struct member, the derivation of the pointer and all use thereof will occur without any intervening action which accesses or addresses that part of the structure via other means. Since the question of exactly when compilers recognize that a pointer is freshly derived would have been seen as a Quality of Implementation issue...Naturalist
...outside the Standard's jurisdiction, there would have been little perceived point in having the Standard permit the use of freshly visibly derived pointers to access objects. It would have been obvious to anyone who wasn't being obtuse that a quality compiler that can see that a pointer is derived from one of another type should allow for the possibility that it might access an object of that type, but pointless to require that compilers that recognize such derivation honor it without requiring that compilers recognize the derivation.Naturalist
Huh. In your new example, I think the plain language of the standard requires them to allow for the possibility that *p2 may be the same object as bar->x, simply because both are of the same type int.Hadik
Maybe a better illustration of my example would be void test3(int *p) { *p = 17; b1 = b2 } and then called as test3(&b2.y);. I think the question is whether an access to a member of a struct is also considered an access to the struct itself. Common sense would seem to say yes.Hadik
@NateEldredge: The Standard states that an lvalue of structure type may be used to access an object of member type, but does not specify the reverse. If an access via lvalue or pointer that is freshly visibly derived from a structure or union type is recognized as being an access to the structure or union object, that will correctly handle most practical relevant cases better than would a general rule allowing structs or unions to be accessed by arbitrary lvalues of member types.Naturalist
Okay, now I understand your point. So I think we agree that it seems to be an oversight that the standard doesn't specify any case where an access to a member must be recognized as an access to the structure, and we agree that there ought to be at least some cases where such a guarantee exists. We may disagree as to which cases those ought to be. (See also N1520 for C.)Hadik
@NateEldredge: The "effective type" or "established type" abstractions are fundamentally broken. If one wants to try to formalize guarantees, then should matter are the sequence and whereabouts of actions which reference objects of different types in different ways. Really, though, most conflict surrounding this issue could have been avoided with a footnote saying "Quality implementations will refrain from using this rule as an excuse to behave in needlessly useless fashion", since most conflicts arise from implementations that do precisely that.Naturalist
@NateEldredge: Incidentally, the idea that writing storage as a type would permanently lock the type of that storage was rejected, but neither clang or gcc reliably handles situations in which storage is written and read as one type, written and read as another type, and then written and read as the original type. If the last action which writes storage with the first type, stores a bit pattern that matches the last value written using the second type, clang and gcc may optimize out all of the operations that rewrote the storage using the original type, and then assume that...Naturalist
...no operations that wrote storage using the second type can affect the value seen by the final read of the first type [even though in the source code the storage had last been written using the first type].Naturalist

© 2022 - 2024 — McMap. All rights reserved.