29

This question is a follow-up to: Is adding to a "char *" pointer UB, when it doesn't actually point to a char array?

In CWG 1314, CWG affirmed that it is legal to perform pointer arithmetic within a standard-layout object using an unsigned char pointer. This would appear to imply that some code similar to that in the linked question should work as intended:

struct Foo {
    float x, y, z;
};

Foo f;
unsigned char *p = reinterpret_cast<unsigned char*>(&f) + offsetof(Foo, z); // (*)
*reinterpret_cast<float*>(p) = 42.0f;

(I have replaced char with unsigned char for greater clarity.)

However, it seems that the new changes in C++17 imply that this code is now UB unless std::launder is used after both reinterpret_casts. The result of a reinterpret_cast between two pointer types is equivalent to two static_casts: the first to cv void*, the second to the destination pointer type. But [expr.static.cast]/13 implies that this produces a pointer to the original object, not to an object of the destination type, since an object of type Foo is not pointer-interconvertible with an unsigned char object at its first byte, nor is an unsigned char object at the first byte of f.z pointer-interconvertible with f.z itself.

I find it hard to believe that the committee intended a change that would break this very common idiom, making all pre-C++17 usages of offsetof undefined.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
  • I would think that `p` is a pointer with the address of `f.z`, and since `f.z` is a float, using it through a `float*` would not violate strict aliasing (And all standard layout object addresses are able to be stored as a `unsigned char*`, as this is the object representation) – Artyer Apr 08 '19 at 17:20
  • 1
    @Artyer Strict aliasing is not the issue here. The strict aliasing rule existed before C++17, and `std::launder` has nothing to do with it. – Brian Bi Apr 08 '19 at 17:27
  • 2
    @Brian: "*that this produces a pointer to the original object*" What "original object" are we talking about here? `p` doesn't point to a `Foo` anymore. It points to a byte. It stopped pointing to a `Foo` when you performed pointer arithmetic on a byte array pointer. – Nicol Bolas Apr 08 '19 at 19:10
  • @NicolBolas [expr.static.cast]/13 seems to imply, with the wording "the pointer value is unchanged by the conversion", that `reinterpret_cast(&f)` still points to `f`, rather than to an `unsigned char` object at the beginning of `f`. – Brian Bi Apr 08 '19 at 19:12
  • 1
    @Brian: But we're not talking about the result of that reinterpret cast; we're talking about the result of the *pointer arithmetic* of the byte pointer. – Nicol Bolas Apr 08 '19 at 19:20
  • @NicolBolas If you have an `unsigned char*` whose value is a pointer to a `Foo` object (rather than a pointer to an `unsigned char` object), then it seems that doing pointer arithmetic using that pointer is illegal ([expr.add]/6). `std::launder` seems like it would fix this, by forcing the pointer to point to a `unsigned char` rather than a `Foo`. – Brian Bi Apr 08 '19 at 19:24
  • 2
    @Brian: But CWG1314 you cited it, says otherwise. Nothing in C++17 changed either of those, so it's not clear what you're concerned about with regard to the arithmetic. – Nicol Bolas Apr 08 '19 at 19:48
  • @NicolBolas C++17 appears to have changed the semantics of pointer values, so that now, `reinterpret_cast` no longer does what it used to do, unless there is a pointer-interconvertible object of the target type. Presumably, CWG 1314 still applies in the sense that once you already *have* a pointer to an `unsigned char` object, you can do arithmetic on it. But C++17 seems to change the requirements for obtaining that pointer value. – Brian Bi Apr 08 '19 at 19:50
  • 2
    https://wg21.link/CWG1701. CWG doesn't seem to have discussed that issue recently, but the most recent discussion from a few years back do not appear to have a clear direction. – T.C. Apr 10 '19 at 06:36
  • 1
    @T.C. So CWG changed its mind between 2011 and 2013? Any idea why? – Brian Bi Apr 10 '19 at 18:01
  • `launder` won't help you, because it requires an object of `unsigned char` type to be alive at the address represented by the argument of `launder` – Language Lawyer Apr 13 '19 at 03:41
  • @LanguageLawyer the CWG1314 resolution affirms that every object can be considered to have an `unsigned char` object alive at each byte of it – M.M Jun 05 '19 at 01:22
  • @M.M there is no resolution of CWG1701 which reaffirms CWG1314 resolution. Also, your interpretation of CWG1314 resolution is not compatible with http://timsong-cpp.github.io/cppwp/n4659/intro.object#8 – Language Lawyer Jun 05 '19 at 08:52
  • @LanguageLawyer CWG resolutions resolve issues with the standard , if something seems "not compatible" it means the committee says the standard was wrong or should be interpreted differently than you think – M.M Jun 06 '19 at 01:20
  • @M.M I'm not an expert here, but how I understand it: if wording incompatible with previous CWG resolutions (or conclusions from it) is added (that wording was added after the resolution, wasn't it?), this cancels those resolutions (or conclusions from it). – Language Lawyer Jun 06 '19 at 08:13
  • @LanguageLawyer in this case the resolution says that they think the wording already specifies what the resolution says and so no changes are needed – M.M Jun 06 '19 at 08:22
  • @M.M anyway, an object of `unsigned char` can't be alive at the first byte of another alive object (except for special cases like struct with the first member of `unsigned char` type etc.) per https://timsong-cpp.github.io/cppwp/n4659/intro.object#8 – Language Lawyer Jun 06 '19 at 08:37
  • @LanguageLawyer according to the committee, you're reading intro.object#8 wrong and the unsigned char object is alive. – M.M Jun 06 '19 at 21:52
  • @M.M if [intro.object]/8 was added later than the resolution of CWG1314, it doesn't have to apply to [into.object]/8. – Language Lawyer Jun 07 '19 at 09:48

1 Answers1

1

You question was:

Do we need to use std::launder when doing pointer arithmetic within a standard-layout object (e.g., with offsetof)?

No.

std::launder won't change anything in this case and therefore has nothing to do with the presented example (imo edit launder out of the question or ask another question).

std::launder is usually just needed in a subset of cases (eg. due to a const member) where you change (or create) an underlying object in some runtime manner (eg. via placement new). Mnemonic: the object is 'dirty' and needs to be std::launder'ed.

Using only a standard layout type cannot result in a situation where you would ever need to use std::launder.

darune
  • 10,480
  • 2
  • 24
  • 62
  • 2
    `size_t` should represent size of an object and may be not enough to represent a pointer. You should use `uintptr_t` or `intptr_t`. – KamilCuk Jun 14 '19 at 13:56
  • The mapping from integral to pointer type is anyway implementation-defined, so whether your code will behave as expected is implementation-defined. In any case, I don't see how the answer relates to the question. There is no cast to integral type in the question. – walnut Feb 21 '20 at 11:17
  • @walnut it was a kind of proof by contradiction but I guess you are right. I updated the answer. – darune Feb 21 '20 at 15:40