6

Is there any case where functions foo() and optimized_foo() in the example below are not equivalent?

struct Test
{
    int     x;
    int     y;
    int     z;
};

// Some external function. Implementation unknown.
void bar(int& arg);

void foo()
{
    Test      t;
    t.x = 3;
    t.y = 4;
    t.z = 5;
    bar(t.y);
}

void optimized_foo()
{
    int       t_y = 4;
    bar(t_y);
}

It's just that all major x86_64 compilers (gcc 10.2, clang 10.0, msvc 19.24) keep the initialization of t.x and t.z in assembler code generated for foo() even at the highest optimization level. Even though those members are obviously not used. Do they have a reason?

Am I right assuming that bar(), being given a reference to one data member of a structure, has no legal way to obtain a pointer/reference to other members? What does the standard say about it?

Igor G
  • 1,838
  • 6
  • 18
  • 3
    [This](https://stackoverflow.com/questions/60335046/casting-from-member-pointer-to-whole-struct-class) and [this](https://stackoverflow.com/questions/33870219/get-pointer-to-object-from-pointer-to-some-member) questions suggest that for a standard layout type it is legal to access other members via a pointer to one of them. – Evg Jul 31 '20 at 16:13
  • 1
    i would guess that it is an aliasing issue, since you could possibly get a reference/pointer to the struct from the reference to the member like detailed in [this question](https://stackoverflow.com/a/48303745/8411406). – Turtlefight Jul 31 '20 at 16:14
  • 1
    @Evg thank you for the link! I totally agree that if `y` was the first data member of a standard layout struct, then a legal conversion would exist. But it's not the first member. And I strongly suspect that subtracting `offsetof` from the pointer to bar's argument (as discussed in the linked question) is UB. – Igor G Jul 31 '20 at 16:21
  • @IgorG: What makes you think that something is "stopping" an optimization from happening? I guess I'm just not sure why this all matters. – Nicol Bolas Jul 31 '20 at 16:23
  • @IgorG, I suspect that too but not 100% sure. Anyway, optimizing `t` away is likely to break some existing code. – Evg Jul 31 '20 at 16:26
  • @NicolBolas it's my gut feeling. Those compilers are usually pretty good at optimizing unused variables. And if I change the signature of bar to `bar(int)` then they will indeed optimize out t.x and t.z. The fact that they are not doing it if t.y is passed by reference makes me suspect there's some strong reason to it. I would like to understand that reason, if only for the sake of my own knowledge level. – Igor G Jul 31 '20 at 16:28
  • "*they will indeed optimize out t.x and t.z*" Does it optimize those members out, or does it simply optimize out `t` itself, instead passing a literal value? Because those are two different things. – Nicol Bolas Jul 31 '20 at 16:29
  • @NicolBolas, they just call `bar(4)` with tail call optimization, too. Can't say for sure what exactly is optimized out -- `t` or its unused members. – Igor G Jul 31 '20 at 16:33

1 Answers1

5

bar could take the address of the the member through the reference [expr.unary.op]. The function could then copy the bytes of the object representation of the adjacent members.

void bar(int& arg) {
    constexpr auto size      = sizeof(Test);
    constexpr auto offset    = offsetof(Test, y);
    constexpr auto remaining = size - offset;
    unsigned char buffer[remaining];
    
    std::memcpy(buffer, &arg, remaining);
}

At the end of the function, the buffer contains object representation of some members of Test object. Given that bar is defined externally, the compiler could have no way of knowing whether the memory of the other members is observed or not when compiling foo.

Note: offsetof is only conditionally supported for non-standard-layout types. The class in question is standard layout.

[basic.types]

For any object (other than a potentially-overlapping subobject) of trivially copyable type T, whether or not the object holds a valid value of type T, the underlying bytes ([intro.memory]) making up the object can be copied into an array of char, unsigned char, or std​::​byte ([cstddef.syn]). If the content of that array is copied back into the object, the object shall subsequently hold its original value.

The object representation of an object of type T is the sequence of N unsigned char objects taken up by the object of type T, where N equals sizeof(T). ...


P.S. I used the strange example observing only the successive members because observing the preceding members would require explicit pointer arithmetic which is somewhat ambiguously specified in the standard. I see no practical problems with doing that, but I left it out from the example to keep the issue separate. See related post.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • Note that to pull this off, you have to specifically avoid doing classic UB things like incrementing just the `int*` to access the neighboring `int` or using `offsetof` to try to convert the reference to `y` back into a glvalue of the total `Test` object. – Nicol Bolas Jul 31 '20 at 16:55
  • @NicolBolas As long as the pointer arithmetic with `char` is fine, referring to the `Test` should be fine as long as the reinterpreted pointer is laundered, right? – eerorika Jul 31 '20 at 17:00
  • "*As long as the pointer arithmetic with char is fine*" Well, that is the question. The mere existence of [P1839](https://wg21.link/P1839) casts into doubt the validity of doing pointer arithmetic to access the object representation of objects in C++ prior to that proposal. And even it only allows you to, given a pointer to a `T`, access the object representation of that `T` and any of its subobject, *not* any siblings of the `T`. – Nicol Bolas Jul 31 '20 at 17:04
  • @NicolBolas `Well, that is the question` That is a question covered here: https://stackoverflow.com/q/47498585/2079303 – eerorika Jul 31 '20 at 17:06
  • @eerorika, thank you, this example explains the behavior that I see. – Igor G Jul 31 '20 at 17:07
  • @eerorika: If that answer were correct, P1839 would not have to exist, would it? Yet it does exist and is getting through standardization. So clearly, despite CWG1314's apparent ruling, the committee feels that the wording needs to be changed. – Nicol Bolas Jul 31 '20 at 17:11
  • @NicolBolas My interpretation of the answers is that current wording of the standard fails to clearly state that it is allowed. Thus, I see no reason why a proposal to change it shouldn't exist. – eerorika Jul 31 '20 at 17:14
  • @NicolBolas the funny thing is that even P1839 doesn't allow getting to the whole `Test` object from a pointer to its `Test::y` subobject. – Language Lawyer Aug 01 '20 at 08:17
  • @LanguageLawyer: That's not the point of the feature (though it could be changed fairly easily to allow it to some limited degree). – Nicol Bolas Aug 01 '20 at 13:23