0

For this discussion, I assume that scalar objects are things like ints, floats, chars, bools, and pointers. Non-scalar objects are aggregates (structs) that are made up of scalar types and aggregates recursively.

Given this assumption, do C++ programs ever access aggregates distinctly from their scalar components?

As an example:

struct s { int a; float b; };
void assign1(s& out, s const& in) { out = in; }
void assign2(s& out, s const& in) { out.a = in.a; out.b = in.b; }

Clearly assign1 and assign2 are equivalent in practice, and both access the int s::a and float s::b. But do either of them also access the whole aggregate in any sense?

The interpretation that only scalar objects are ever actually accessed has interesting consequences.

For instance, according to the resolution of my other question here, forming a reference to an object does not constitute access. Given that resolution, I can write a function like this:

void assign3(s& out, s const& in) {
    int& a_out = out.a;  // no access
    int const& a_in = in.a;  // no access
    a_out = a_in;  // access some ints
}

No "access" occurs except on the third line, which accesses some ints. Whether out and in actually refer to objects of s type is inconsequential. Only a_out and a_in must actually refer to ints.

Given this, and the fact that an object's address is the address of its first non-static data member, I am within my rights to write

int out, const in = 42;
assign3(reinterpret_cast<s&>(out), reinterpret_cast<s const&>(in));

If all of these assumptions hold, then C and C++ are for the most part just portable assembly language and aliasing rules are just to help the compiler correctly read registers out of the x87 floating point coprocessor.

Of course the assumptions don't hold. I'm wrong. But why am I wrong? Why do the standard documents have all these rules about effective type or dynamic type?

Given struct a { int a; }; struct b { int b; }; what benefit is there to making access of a::a through a b::b undefined except in some limited situations involving unions?

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
Filipp
  • 1,843
  • 12
  • 26
  • 1
    Does this answer your question? [What happened to the "aggregate or union type that includes one of the aforementioned types" strict aliasing rule?](https://stackoverflow.com/questions/56878519/what-happened-to-the-aggregate-or-union-type-that-includes-one-of-the-aforement) – Language Lawyer Oct 02 '20 at 03:09
  • There is an interesting inversion in the question: the language does not make anything undefined. It's the other way around: initially, nothing is defined. Then the standard makes some things defined. It sometimes underlines that it's leaving out some things, or explicitely leaves out things that would have otherwise been covered by a rule. – spectras Oct 02 '20 at 07:53
  • You are correct that the document does not "undefine" things. But the people who compose that document leave things out, usually for good reason. They think something is gained by making aspects of behavior "undefined". I'd like to know why. – Filipp Oct 02 '20 at 11:09
  • @LanguageLawyer That question is related, but it does not address whether `object.member` constitutes access of just the `member` or the whole `object`. – Filipp Oct 02 '20 at 11:12
  • 1
    Maybe [this](https://timsong-cpp.github.io/cppwp/n4861/basic.lval#11.sentence-3) and/or [this](https://timsong-cpp.github.io/cppwp/n4861/defns.access#sentence-2) will satisfy you? – Language Lawyer Oct 02 '20 at 14:02
  • Perfect! Thanks for finding that. Then how can modern compilers have meaningful TBAA for any types other than primitive scalars? – Filipp Oct 02 '20 at 14:06
  • Unfortunately, it is not clearly specified in [expr.ref] (but should), that in a class member access expression `E1.E2`, if `E1` has type `T`, then it should denote the object of type `T` (so `reinterpret_cast(&y)->x` in the linked question is UB) – Language Lawyer Oct 02 '20 at 14:19
  • I'd like to clarify my understanding of your comment. Are you saying that **if** the standard required `E1` to actually denote an object of its declared type for a member access expression `E1.E2` to be defined, **then** the `reinterpret_cast` would be UB? – Filipp Oct 02 '20 at 17:30
  • Use @LanguageLawyer, otherwise I don't get notifications. `reinterpret_cast` won't be UB, but a member access expression with bad object expression would be UB. – Language Lawyer Oct 02 '20 at 21:10
  • Why would I want to pollute your inbox with notifications about my silly question? Also thanks for the insightful comments. It seems that my question has two parts: do c++ programs access non-scalars, and is what implication that has for aliasing, tbaa, and undefined behavior. You answered the former question: c++ programs only ever access scalars. I'll accept that answer. Perhaps the rest deserves a separate discussion. Perhaps not? – Filipp Oct 03 '20 at 03:52
  • @Filipp: Most questions involving Undefined Behavior involve situations where parts of the C or C++ Standard, in conjunction with documentation for a platform or implementation, would together specify how something would behave, but some *other* part of the Standard would imply that it invokes Undefined Behavior. If there were truly nothing that defined the behavior, people would have no reason to expect anything in particular to happen. – supercat Oct 04 '20 at 17:27
  • @Filipp: Given a definition like `struct foo { int bar[10]; } *p;`, clang and gcc won't consistently process constructs like `p->bar[index];` in cases where `p` doesn't point to a `foo`, even if `((int*)p)+index` would point to an `int`, so not everything is semantically decomposable into scalar accesses. – supercat Oct 05 '20 at 00:14
  • @supercat Is that true even when `p` points to `int[10]`? – Filipp Oct 07 '20 at 11:52
  • @Filipp: Yup. If one has pointers to two different structure types, both defined as containing single member `int ArrayMember[10]`, neither clang nor gcc will allow for the possibility that an lvalue using syntax `p1->arrayMember[index]` might identify the same storage as `p2->arrayMember[index]` – supercat Oct 07 '20 at 16:34
  • See https://godbolt.org/z/c6q1eT for an example of where gcc fails to make such an allowance. Clang seems to handle arrays a little better in this case, though I don't think it always refrains from making assumptions about aliasing of the parent object. – supercat Oct 07 '20 at 22:42
  • That's an observation of the classic common initial sequence union aliasing bug that GCC has: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65892 . According to the links LanguageLawyer posted, the standard mandates that only scalars are accessed. But that would imply that the common initial sequence aliasing of unions does not need any special case. According to that interpretation, current compilers are wrong. – Filipp Oct 08 '20 at 01:36
  • Compiler implementers read the presence of that special case as license to apply nominal (not structural) TBAA everywhere else. One or the jobs of standards is to standardize existing practice. If all viable implementations of C++ behave a certain way, wouldn't standardizing that behavior in clear specific language be more useful than wondering what the standard means? – Filipp Oct 08 '20 at 01:39
  • @Filipp: The intention of the original C Standard was to allow implementations to assume things won't alias in cases where there's no apparent relationship between them, but require that relationships be presumed among objects of certain type. The ability to recognize other relationships was regarded as a quality-of-implementation issue outside the Standard's jurisdiction. If `p1` and `p2` are pointers to different types, and the sequence `p1->arr[0]=1; p2->arr[0]=2; return p1->arr[0];` executes *without any intervening actions that would suggest an association between the two pointers*... – supercat Oct 08 '20 at 16:00
  • ...having a compiler return 1 rather than reloading `p1->arr[0]` would be an appropriate optimization for most quality implementations, but if other intervening steps would suggest a relationship, it wouldn't be. The problem is that people have been arguing for too long about what optimizations are allowed, rather than recognizing that the Standard deliberately allows implementations specialized for some purposes to behave in ways that make them unsuitable for others, unavoidably allowing low-quality implementations to behave in ways that would make them unsuitable for much of anything. – supercat Oct 08 '20 at 16:03
  • @Filipp: If the argument had been properly framed, as being about whether the Standard would allow a garbage-quality-but-conforming implementation to process a construct in meaningless fashion, that would have made it clear that the efficiency with which a C compiler can process only programs that the Standard would require it to should be the primary measure of merit upon which compilers are judged. – supercat Oct 08 '20 at 16:11

1 Answers1

1

One of the things that matters to compilers is reloading registers from memory. That takes time, and is best avoided. So if you know that at address p a Foo structure lives that contains a float[2], you have the first float in a register, and you then have write to a float at address q, do you need to reload that first float ? That might be necessary if you can't prove p!=q. But if you know that the float at address q is part of a Bar and therefore followed by an int, then that is proof that p!=q. So a write to q does not force a reload from p.

Note that the data following p and q is not read or written here. It's only the fact that those types differ which allows the compiler to optimize out the redundant read through p.

MSalters
  • 173,980
  • 10
  • 155
  • 350
  • That is true. But if I only access `p->floats[0]` and never access `p->floats[1]`, can the compiler actually assume that a complete `Foo` object exists there? According to my scalar types interpretation, there's no such thing as accessing `Foo`. Only accessing `floats`. By that logic, as long as no data following `p` or `q` is accessed, they could in fact alias. – Filipp Oct 02 '20 at 11:17
  • 2
    @Filipp: In Dennis Ritchie's original language, any region of storage would simultaneously contain all objects that will fit. Something like `foo->bar` would take the address in `foo`, add the offset of struct member `bar`, and access an object of `bar`'s type at the resulting address, without regard for whether any structure of `foo`'s type actually existed. C++ adds additional requirements implying that behavior would only only defined if an object of the structure type actually exists, but with some ambiguous corner cases which will never be resolved because there's no consensus... – supercat Oct 04 '20 at 17:32
  • 2
    ...about what the Standard means, and nobody will agree to a proposed wording that would be incompatible with their interpretation. – supercat Oct 04 '20 at 17:33
  • @Filipp: The access to `p->floats[0]` requires that `p->` is legal, which generally does require that a complete `Foo` object exists there. (In theory, a Foo ctor could have `p=this; p->floats[0]=0;`, and so could a dtor, but that is an exception which I don't think you were looking for). The `p->` thing is not a scalar "access", but it's necessary to reach that salar. – MSalters Oct 06 '20 at 07:37
  • @supercat: True. Personally I'm siding with the people who argue that there is a large set of valid C programs which are still valid C++. Hence, if there's any ambiguity in ISO C++ whether "malloc, cast, write" is valid, that should be resolved in favor of running code. – MSalters Oct 06 '20 at 07:41
  • @MSalters: The C Standard makes no pretense of defining everything necessary for an implementation to be suitable for all purposes. The C++ Standard does make such a pretense, but nonetheless explicitly states that it does not define a category of conforming C++ programs, but merely specifies constructs that C++ implementations must handle to be conforming. I think it entirely reasonable to say that something can be a conforming C++ implementation while being incompatible with many C constructs, but an implementation that claims to be suitable for processing C-based code should... – supercat Oct 06 '20 at 15:01
  • ...handle constructs that are common within such code *regardless of whether the Standard mandates it or not*. – supercat Oct 06 '20 at 15:01
  • @MSalters: What's needed is an "official" recognition of Quality of Implementation as a concept. The question of exactly *what* makes something a quality implementation is and should be outside the Standards' jurisdiction, but the ability to handle constructs beyond what the Standard should be recognized as making the implementation superior for tasks where those constructs would be useful. – supercat Oct 06 '20 at 15:04
  • @MSalters "The access to `p->floats[0]` requires that `p->` is legal" Does it though? `p->floats[0]` does not by itself perform any access. When I assign the result of that expression to a `float` object, the program computes an address and reads a float from that address. The standard certainly claims that a `float` must exist at that address for the access to be defined, but I don't believe `Foo` is necessary for this. – Filipp Oct 07 '20 at 11:49
  • @Filipp: Regardless of how you or I might interpret the Standard, in the language *actually processed* by clang and gcc, if one structure types `s1 and `s2` both start with `float arr[4];`, and one has pointers `s1 *p1; s2 *p2;` which identify the same storage, values written to `p1->arr[0]` will not reliably appear in `p2->arr[0]`; the maintainers of those compilers insist that such behavior is justifiable given *their* interpretation of the Standard. – supercat Oct 07 '20 at 14:45
  • @Filipp: The authors of the C Standard, in their published Rationale, recognize that they cannot prevent the contrivance of implementations which are "conforming" but of such poor quality that they "succeed at being useless". I don't think the C or C++ Standard should require that `p1->arr[0]` and `p2->arr[0]` be presumed capable of aliasing when there is no evidence of any relationship between them *but* quality compilers should make a bona fide effort to notice evidence of relationships that may exist. In that light, clang and gcc are garbage-quality-but-conforming implementations. – supercat Oct 07 '20 at 14:54
  • @Filipp: If, instead of arguing about what the Standard requires, people twenty years ago had instead pointed out that what the authors of gcc (and later clang) were fighting for was the right to produce garbage-quality-but-conforming implementations, C and C++ would be much better languages today. – supercat Oct 07 '20 at 14:56