5

This question is about pointers derived using pointer arithmetic with struct offsets.

Consider the following program:

#include <cstddef>
#include <iostream>
#include <new>

struct A {
  float a;
  double b;
  int c;
};

static constexpr auto off_c = offsetof(A, c);

int main() {
  A * a = new A{0.0f, 0.0, 5};
  char * a_storage = reinterpret_cast<char *>(a);
  int * c = reinterpret_cast<int *>(a_storage + off_c));

  std::cout << *c << std::endl;

  delete a;
}

This program appears to work and give expected results on compilers that I tested, using default settings and C++11 standard.

(A closely related program, where we use void * instead of char * and static_cast instead of reinterpret_cast, is not universally accepted. gcc 5.4 issues a warning about pointer arithmetic with void pointers, and clang 6.0 says that pointer arithmetic with void * is an error.)

Does this program have well-defined behavior according to the C++ standard?

Does the answer depend on whether the implementation has relaxed or strict pointer safety ([basic.stc.dynamic.safety])?

Chris Beck
  • 15,614
  • 4
  • 51
  • 87
  • is this a homework assignment? – Garr Godfrey Sep 08 '17 at 20:15
  • 2
    @GarrGodfrey: I would be surprised... – Andreas H. Sep 08 '17 at 20:15
  • 2
    Why not use the pointer to member functionality of C++. It is the "clean" alternative to `offsetof` ? – Andreas H. Sep 08 '17 at 20:17
  • Nope, I graduated, no more homework for me :) I guess I did pose the question in a homeworky style. Sometimes when you ask standards questions its best to make an open ended question with a specific code example rather than start explaining what you think you know, in my limited experience – Chris Beck Sep 08 '17 at 20:17
  • I would also say that `char` pointer arithmetic is right here, as `offsetof` is guaranteed to return the offset in units of ` char`'s size (i.e. one byte). – Andreas H. Sep 08 '17 at 20:20
  • 1
    Don't do pointer arithmetic with `void*`. Pointer arithmetic with any `T*` requires `sizeof(T)`, but `void` has no size. – John Sep 08 '17 at 20:22
  • I do not see any reason why it should not be. Looks very correct to me. – SergeyA Sep 08 '17 at 20:27
  • @GarrGodfrey why is it relevant if it's a homework assignment or not? – xaxxon Sep 08 '17 at 20:29
  • as for the void pointer arithmetic, how far away is "5 voids past an address" - so you can see why it doesn't make any sense. – xaxxon Sep 08 '17 at 20:31

3 Answers3

8

There are no fundamental errors in your code.

If A isn't plain old data, the above is UB (prior to C++17) and conditionally supported (after C++17).

You might want to replace char* and int* with auto*, but that is a style thing.

Note that pointers to members do this exact same thing in a type-safe manner. Most compilers implement a pointer to member ... as the offset of the member in the type. They do, however, work everywhere even on non-pod structures.

Aside: I don't see a guarantee that offsetof is constexpr in the standard. ;)

In any case, replace:

static constexpr auto off_c = offsetof(A, c);

with

static constexpr auto off_c = &A::c;

and

  auto* a_storage = static_cast<char *>(a);
  auto* c = reinterpret_cast<int *>(a_storage + off_c));

with

  auto* c = &(a->*off_c);

to do it the C++ way.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Clang will yell at you if you use offsetof on a non-POD type, so that's a nice hint that you're doing things at least somewhat close to correct: https://godbolt.org/g/6jLnM6 - offset of on non-standard-layout type 'A' [-Winvalid-offsetof] – xaxxon Sep 08 '17 at 20:37
  • 1
    Heres what im concerned abt: in 3.7.4.3 [basic.stc.dynamic.safety] it says pointer is safely derived only if (conditions) and if we have strict pointer safety then a pointer is invalid if it doesnt come from such a place. In 5.7 pointer arithmetic, it says i can do usual arithmetic within an array but i dont see anything there telling me struct offset arithmetic is ok. Im trying to figure out if this isnt relevant in the way i think it is, or if struct offset arithmetic is not ok in the hypothetical "strict" impls, or if i read 5.7 wrong (n4296) – Chris Beck Sep 08 '17 at 20:39
  • Basically i think the answer to the question is its ok in relaxed impls and has undefined behavior in a strict impl. I think when i add an index to a pointer that doesnt point to an array, thats automatically not safe in the strict impl, and result is not a safely derived ptr. And it also says dereferencing an invalid ptr is ub. I think that this must somehow be a drafting error. But i could be totally wrong :) – Chris Beck Sep 08 '17 at 21:12
  • 2
    @ChrisBeck I believe the intention of [basic.stc.dynamic.safety] was to define rules for assumptions a garbage collector is allowed to make. basically, you can't obfuscate references to objects and expect them not to be garbage collected. In your case, you still keep a reference to the object ("a") – Arvid Sep 09 '17 at 10:07
4

It is safe in your specific example, but only because your struct is a standard layout, which you can double-check using std::is_standard_layout<>.

Trying to apply this to a struct such as:

struct A {
  float a;
  double b;
  int c;
  std::string str;
};

Would be illegal, even if the string is past the part of the struct that's relevant.

Edit

Heres what im concerned abt: in 3.7.4.3 [basic.stc.dynamic.safety] it says pointer is safely derived only if (conditions) and if we have strict pointer safety then a pointer is invalid if it doesnt come from such a place. In 5.7 pointer arithmetic, it says i can do usual arithmetic within an array but i dont see anything there telling me struct offset arithmetic is ok. Im trying to figure out if this isnt relevant in the way i think it is, or if struct offset arithmetic is not ok in the hypothetical "strict" impls, or if i read 5.7 wrong (n4296)

When you are doing the pointer arithmatic, you are performing it on an array of char, the size of which is at least sizeof(A), so that's fine.

Then, when you cast back into the second member, you are covered under (2.4):

— the result of a well-defined pointer conversion (4.10, 5.4) of a safely-derived pointer value;

  • Ok, the pointer im adding to has type `char *` but in 5.7.4 the block about arithmetic on an array, it says, "if the pointer points to an element of an array object..." there is no array object here though in the sense of standard. There is only an A. Thats also related to strict aliasing issues: object only exists when it is created by the language, not conjured into existence by a cast. So im not sure this counts as safe pointer manipulation in an array. I think from standards point of view there is no array here. Idk for sure tho. – Chris Beck Sep 08 '17 at 21:23
2

you should examine your assumptions.

Assumption #1) offsetof gives the correct offset in bytes. This is only guaranteed if the class is considered "standard-layout", which has a number of restrictions, such as no virtual methods, avoids multiple inheritances, etc. In this case, it should be fine, but in general you can't be sure.

Assumption #2) A char is the same size as a byte. In C, this is by definition, so you are safe.

Assumption #3) The offsetof gives the correct offset from the pointer to the class, not from the beginning of the data. This is basically the same as #1, but a vtable could certainly be a problem. Again, only works with standard-layout.

Garr Godfrey
  • 8,257
  • 2
  • 25
  • 23