11

Section §6.5.3.2 "Address and indirection operators" ¶3 says (relevant section only):

The unary & operator returns the address of its operand. ... If the operand is the result of a unary * operator, neither that operator nor the & operator is evaluated and the result is as if both were omitted, except that the constraints on the operators still apply and the result is not an lvalue. Similarly, if the operand is the result of a [] operator, neither the & operator nor the unary * that is implied by the [] is evaluated and the result is as if the & operator were removed and the [] operator were changed to a + operator. ...

This means that this:

#define NUM 10
int tmp[NUM];
int *i = tmp;
printf("%ti\n", (ptrdiff_t) (&*i - i) );
printf("%ti\n", (ptrdiff_t) (&i[NUM] - i) );

Should be perfectly legal, printing 0 and the NUM (10). The standard seems very clear that both of those cases are required to be optimized.

However, it doesn't seem to require the following to be optimized:

struct { int a; short b; } tmp, *s = tmp;
printf("%ti\n", (ptrdiff_t) (&s->b - s) );

This seems awfully inconsistent. I can see no reason that the above code shouldn't print the sizeof(int) plus (unlikely) padding (possibly 4).

Simplifying a &-> expression is going to be the same conceptually (IMHO) as &[], a simple address-plus-offset. It's even an offset that's going to be determinable at compile time, rather than potentially runtime with the [] operator.

Is there anything in the rationale about why this is so seemingly inconsistent?

Kevin Reid
  • 37,492
  • 13
  • 80
  • 108
Chris Lutz
  • 73,191
  • 16
  • 130
  • 183
  • I've seen tons of hard C and C++ standards questions where I didn't know how to do anything but upvote, favorite and wait to read the answers. It feels weird to actually have written one. – Chris Lutz Feb 05 '11 at 06:53
  • interesting ... MSVC++ prints 4! – abRao Feb 05 '11 at 07:19
  • @Abhi Rao - GCC (4.0) with -Wall -Wextra -Werror compiles and prints 4 with no complaint. – Chris Lutz Feb 05 '11 at 07:24
  • Now that we've eliminated the ill-formed null pointer examples, I don't really see a compelling use case for extending this feature to cover `&->`. Obviously the compiler _can_ (and probably will) optimize out the actual dereferencing and taking the address, but I'm just not seeing a compelling example where some use of `&->` is undefined under the current language rules but would be well defined under new rules that give `&->` the same treatment as `&*`. – James McNellis Feb 05 '11 at 18:09
  • @James McNellis - You could make the `offsetof` hack word with a non-null pointer. And presumably if the standard required it to be optimized the ill-formed back would be more likely to work. – Chris Lutz Feb 05 '11 at 22:55
  • `&s->b - s` is a constraint violation ("both operands are pointers to qualified or unqualified versions of **compatible** complete object types"). (Can't believe nobody pointed that out before.) You'd need to cast to `char*` or so before doing the subtraction. – Daniel Fischer Jun 04 '13 at 14:56

3 Answers3

4

In your example, &i[10] is actually not legal: it becomes i + 10, which becomes NULL + 10, and you can't perform arithmetic on a null pointer. (6.5.6/8 lists the conditions under which pointer arithmetic can be performed)

Anyway, this rule was added in C99; it was not present in C89. My understanding is that it was added in large part to make code like the following well-defined:

int* begin, * end;
int v[10];

begin = &v[0];
end = &v[10];

That last line is technically invalid in C89 (and in C++) but is allowed in C99 because of this rule. It was a relatively minor change that made a commonly used construct well-defined.

Because you can't perform arithmetic on a null pointer, your example (&s->b) would be invalid anyway.

As for why there is this "inconsistency," I can only guess. It's likely that no one thought to make it consistent or no one saw a compelling use case for this. It's possible that this was considered and ultimately rejected. There are no remarks about the &* reduction in the Rationale. You might be able to find some definitive information in the WG14 papers, but unfortunately they seem to be quite poorly organized, so trawling through them may be tedious.

James McNellis
  • 348,265
  • 75
  • 913
  • 977
  • I took null pointers out of the examples, since they were never really what I was concerned about. – Chris Lutz Feb 05 '11 at 07:33
  • I don't see how `NULL` comes into play at all here. In addition, for pointer arithmetic (as long as you don't evaluate the non-existing object) the element just after an array can be used. AFAIR, this is mentioned in several places of the standard. – Jens Gustedt Feb 05 '11 at 09:05
  • @Jens: The original examples in the question used the `NULL` pointer and no well-defined arithmetic may be performed on a null pointer. You can obtain a pointer to the one-past-the-end "element," but you cannot dereference it. For `int v[10];`, only in C99 is it legal to use `&v[10]` or `&*(v + 10)`; in C++ and C90 such code formally yields undefined behavior. – James McNellis Feb 05 '11 at 18:04
  • I'm accepting this because "no one thought it was important" is probably the best answer I'll get without searching the rationale. And if I do want to search the rationale this answer has a nice link. – Chris Lutz Feb 06 '11 at 06:04
2

I think that the rule hasn't been added for optimization purpose (what does it bring that the as-if rule doesn't?) but to allow &t[sizeof(t)/sizeof(*t)] and &*(t+sizeof(t)/sizeof(*t)) which would be undefined behaviour without it (writing such things directly may seem silly, but add a layer or two of macros and it can make sense). I don't see a case where special casing &p->m would bring such benefit. Note that as James pointed out, &p[10] with p a null pointer is still undefined behaviour; &p->m with p a null pointer would similarly have stayed invalid (and I must admit that I don't see any use when p is the null pointer).

AProgrammer
  • 51,233
  • 8
  • 91
  • 143
  • The obvious (IMHO) use when `p = NULL` is the hacky implementation of the `offsetof` macro, which relies on `&((struct t *)0)->m` working. It could, however, just as easily be changed to `1` (or a compiler-dependent valid pointer value like, say, the stack) instead of `0`, and while it would probably be unlikely to give you good `struct` values it ought to give you the right offset value. – Chris Lutz Feb 05 '11 at 08:02
  • @Chris: an aeon ago, I had an early standard C compiler that defined offsetof() in terms off address 0 and then gave either core dumps or compilation errors (I forget which, now) when it was used. I ended up hacking the system header and used 1024 as an address instead of 0; that worked fine. It (1024) is sufficiently aligned not to give problems - unlike 1. – Jonathan Leffler Feb 06 '11 at 15:54
  • Except for character arrays, `&t[sizeof(t)]` reaches far beyond the end of the allocated object. – Jonathan Leffler Feb 06 '11 at 15:55
  • @Jonathan - Oh right, alignment exists. I feel rather silly now. – Chris Lutz Feb 07 '11 at 01:32
1

I believe that the compiler can choose to pack in different ways, possibly adding padding between members of a struct to increase memory access speed. This means that you can't for sure say that b will always be an offset of 4 away. The single value does not have the same problem.

Also, the compiler may not know the layout of a struct in memory during the optimization phase, thus preventing any sort of optimization concerning struct member accesses and subsequent pointer casts.


edit:

I have another theory...

many times the compiler will optimize the abstract syntax tree just after lexical analysis and parsing. This means it will find things like operators that cancel out and expressions that evaluate to a constant and reduce those sections of the tree to one node. This also means that the information about structs is not available. later optimization passes that occur after some code generation may be able to take this into account because they have additional information, but for things like trimming the AST, that information is not yet there.

Scott M.
  • 7,313
  • 30
  • 39
  • 1
    You can't be sure it'll always be an offset of 4, but for a `struct` to be useful you can be sure it'll be a constant offset. And I used an `int` followed by a `short`, so I doubt there's a compiler that needs to put padding in between them. – Chris Lutz Feb 05 '11 at 06:56
  • "Also, the compiler may not know the layout of a struct in memory during the optimization phase..." That seems like some pretty essential information for an optimizer to have. – Chris Lutz Feb 05 '11 at 06:58
  • i think it would also depend on how the compiler is written. The standard specifies rules for how it should work normally, but setting an optimization flag probably does do as you say. My guess is that the writers of the standard didn't want to impose too much optimization. – Scott M. Feb 05 '11 at 07:07
  • It seems like `&s->b` is less optimization than `&i[x]`. `&s->b` is the same as `(typeof_b *)((char *)s)[offsetof(typeof_s, b)]`, and since `offsetof` is an integer constant expression (17.7 ¶3) this seems like it'd be even more likely to be optimizable. – Chris Lutz Feb 05 '11 at 07:19
  • I agree with Scott's answer that the single value doesn't have the same problem but for a different reason. Conceptually you're right, but you're thinking inhuman terms which doesn't always map to compiler functionality. To put it semantically you're trying to address an offset of nothing. – Dark Star1 Feb 05 '11 at 07:22
  • @Dark Star1 - The fact that I used NULL pointers is irrelevant, and I took them out of the examples because everyone's getting hung up on that. – Chris Lutz Feb 05 '11 at 07:34
  • This question (as I understand it) has nothing to do with optimization in a compiler. This question has to do with how expressions involving the `*`, `&`, and `->` operators are defined and why there is a potential inconsistency. In any case, the first statement here is wrong: within a single translation unit, a given struct type has a fixed size and its members are at well-defined offsets that cannot change. The discussion of phases of compilation and what information the compiler has available at each phase is, in my opinion, quite irrelevant. – James McNellis Feb 05 '11 at 18:24
  • The question (as I understand it) required a **reason** for the inconsistency. Therefore, I have submitted my best guess as to why the creators of the standard did not require those optimizations. How the compiler will optimize is definitely relevant to how the standard is written. Sadly, I can't go back and observe them writing the standard, so I have submitted my best guess. If the compiler had perfect information, it could create the perfectly organized and optimized program. Since it may not have access to that future information, it makes sense to include that in my answer. – Scott M. Feb 05 '11 at 19:19