15

Since C++11 std::complex<T>[n] is guaranteed to be aliasable as T[n*2], with well defined values. Which is exactly what one would expect for any mainstream architecture. Is this guarantee achievable with standard C++ for my own types, say struct vec3 { float x, y, z; } or is it only possible with special support from the compiler?

yuri kilochek
  • 12,709
  • 2
  • 32
  • 59
  • 5
    We have a special language exception to type aliasing for a standard library type? Learn something new every day. – Barry Feb 23 '17 at 16:17

3 Answers3

9

TL;DR: The compiler must inspect reinterpret_casts and figure out that (standard library) specializations of std::complex are involved. We cannot conformably mimic the semantics.

I think it's fairly clear that treating three distinct members as array elements is not going to work, since pointer arithmetic on pointers to them is extremely restricted (e.g. adding 1 yields a pointer past-the-end).

So let's assume vec3 contained an array of three ints instead. Even then, the underlying reinterpret_cast<int*>(&v) you implicitly need (where v is a vec3) does not leave you with a pointer to the first element. See the exhaustive requirements on pointer-interconvertibility:

Two objects a and b are pointer-interconvertible if:

  • they are the same object, or

  • one is a standard-layout union object and the other is a non-static data member of that object ([class.union]), 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 ([class.mem]), or

  • there exists an object c such that a and c are pointer-interconvertible, and c and b are pointer-interconvertible.

If two objects are pointer-interconvertible, then they have the same address, and it is possible to obtain a pointer to one from a pointer to the other via a reinterpret_­cast. [ Note: An array object and its first element are not pointer-interconvertible, even though they have the same address.  — end note ]

That's quite unequivocal; while we can get a pointer to the array (being the first member), and while pointer-interconvertibility is transitive, we cannot obtain a pointer to its first element.

And finally, even if you managed to obtain a pointer to the first element of your member array, if you had an array of vec3s, you cannot traverse all the member arrays using simple pointer increments, since we get pointers past-the-end of the arrays in between. launder doesn't solve this problem either, because the objects that the pointers are associated with don't share any storage (cf [ptr.launder] for specifics).

Columbo
  • 60,038
  • 8
  • 155
  • 203
  • 2
    How does `launder` help? Also, why is an array not interconvertible to its first element? (These may be too broad for comments... ) – Barry Feb 23 '17 at 16:41
  • @Barry The latter is perhaps inspired by an optimization. You are right concerning the former, I forgot checking that the struct is packed. – Columbo Feb 23 '17 at 16:45
  • Oh joy, an array pointer that is at most a `ForwardIterator`. And one that is so dirty a hack that it requires `launder`ing at each step. – yuri kilochek Feb 23 '17 at 17:23
  • C++ could use a "layout intercompatible" assertion or something. As in, claim that a `struct bob { int x; int y; }` is layout intercompatible with `int[2]` where `x` is `[0]` and `y` is `[1]`, and that `bob[17]` is layout intercompatible with `int[34]` in a similar way. I say "assertion" because I simply want the power to *request* it, and if the compiler says no at least it cleanly breaks at build time. – Yakk - Adam Nevraumont Feb 23 '17 at 20:56
  • 1
    I think there's a problem with the second `launder` in your alternative solution. The first `launder` will indeed give you a pointer to `vecarr[0].a`, but that `int` object is not an array element, so `p + 1` doesn't *point to* `vecarr[0].b`, it is a *pointer past the end* of `vecarr[0].a`. Because of this, no bytes of storage are *reachable* through `p + 1` ([\[ptr.launder\]/3](http://eel.is/c++draft/ptr.launder#3)). – bogdan Feb 23 '17 at 22:01
  • 1
    However, the bytes in the storage of `vecarr[0].b` would be reachable through the `launder`ed value, so the reachability requirement in [\[ptr.launder\]/1](http://eel.is/c++draft/ptr.launder#1) is not satisfied. As far as I understand, you basically cannot `launder` a pointer past the end and get anything useful. – bogdan Feb 23 '17 at 22:01
  • 2
    @bogdan I did not look at `launder`'s wording after P0137r0. You are right. In fact, flat iteration of multi dimensional arrays is completely impossible, that's quite the pity! – Columbo Feb 24 '17 at 00:17
  • @yurikilochek See the updated answer, btw. I think the motivation behind this restriction are useful optimizations like dead store elimination. Although I really don't see why complex is so special, in the general case, should you need flat iteration, you should create one single array and instead write a wrapper to facilitate access of specific "slices" (as in valarray). – Columbo Feb 24 '17 at 12:30
5

It's only possible with special support from the compiler, mostly.

Unions don't get you there because the common approach actually has undefined behaviour, although there are exceptions for layout-compatible initial sequences, and you may inspect an object through an unsigned char* as a special case. That's it, though.

Interestingly, unless we assume a broad and useless meaning of "below", the standard is technically contradictory in this regard:

[C++14: 5.2.10/1]: [..] Conversions that can be performed explicitly using reinterpret_cast are listed below. No other conversion can be performed explicitly using reinterpret_cast.

The case for complex<T> is then not mentioned. Finally the rule you're referring to is introduced much, much later, in [C++14: 26.4/4].

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • Not sure if that's contradictory. The clause you quoted clearly permits the conversion, but it isn't given the meaning we want just yet (that's what 26.4 does). – Columbo Feb 23 '17 at 16:40
  • 1
    @Columbo: Is it "clear" though? IRTA the conversion is [arguably] not "listed below", so it cannot be performed explicitly. It's like they added a new rule to 26.4/4 but forgot to update 5.2.10. – Lightness Races in Orbit Feb 23 '17 at 16:45
  • 3
    What conversion? The conversion of one object lvalue to an lvalue reference of another object type? Isn't that like paragraph 11? – Columbo Feb 23 '17 at 16:46
4

I think it would work for a single vec3 if your type contained float x[3] instead, and you ensure sizeof(vec3) == 3*sizeof(float) && is_standard_layout_v<vec3>. Given those conditions, the standard guarantees that the first member is at zero offset so the address of the first float is the address of the object, and you can perform array arithmetic to get the other elements in the array:

struct vec3 { float x[3]; } v = { };
float* x = reinterpret_cast<float*>(&v);  // points to first float
assert(x == v.x);
assert(&x[0] == &v.x[0]);
assert(&x[1] == &v.x[1]);
assert(&x[2] == &v.x[2]);

What you can't do is treat an array of vec3 as an array of floats three times the length. Array arithmetic on the array inside each vec3 won't allow you to access the array inside the next vec3. CWG 2182 is relevant here.

Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521