2

I'm looking at the macro offsetof from <cstddef>, and saw that a possible implementation is via

#define my_offsetof(type, member) ((void*) &(((type*)nullptr)->member))

I tried it and indeed it works as expected

#include <iostream>

#define my_offsetof(type, member) ((void*) &(((type*)nullptr)->member))

struct S
{
    char x;
    short y;
    int z;
};

int main()
{
    std::cout << my_offsetof(S, x) << '\n';
    std::cout << my_offsetof(S, y) << '\n';
    std::cout << my_offsetof(S, z) << '\n';

    S s;
    std::cout << (void*) &((&s)->x) << '\n'; // no more relative offsets
    std::cout << (void*) &((&s)->y) << '\n'; // no more relative offsets
    std::cout << (void*) &((&s)->z) << '\n'; // no more relative offsets
}

Live on Coliru

the only modification I've done being that I use a final cast to void* instead of size_t, as I want to display the address as a pointer.

My question(s):

  1. Is the code perfectly legal, i.e. is it legal to "access" a member via a nullptr, then take its address? If that's the case, then it seems that &(((type*)nullptr)->member) computes the address of the member relative to 0, is this indeed the case? (it seems so, as in the last 3 lines I get the offsets relative to the address of s).
  2. If I remove the final cast to (void*) from the macro definition, I get a segfault. Why? Shouldn't &(((type*)nullptr)->member) be a pointer of type type*, or is the type somehow erased here?
vsoftco
  • 55,410
  • 12
  • 139
  • 252
  • It's UB to dereference a null pointer, even if it's only to take its address. – Jonathan Potter Jan 27 '16 at 03:51
  • @JonathanPotter That's what I thought, although wasn't sure. So the cppreference "possible implementation" is broken then. How would you then implement this macro? – vsoftco Jan 27 '16 at 03:52
  • @vsoftco: Not everything that is specified in the C++ standard may be reimplemented by the user. Certain things, like `offsetof` or `std::complex` (for example), *cannot* be implemented in a standards conforming manner. – Cornstalks Jan 27 '16 at 03:55
  • @Cornstalks I see... What's wrong with `std::complex` though? Isn't it just a `struct` with 2 members and some member functions? – vsoftco Jan 27 '16 at 03:57
  • @vsoftco: It's guaranteed to not have any padding and can be cast to a `float[2]` (or `double[2]`) array. [Unfortunately, there is no mechanism in the C++ standard that lets users provide that level of guarantee with their own classes](http://stackoverflow.com/q/22925905/1287251). – Cornstalks Jan 27 '16 at 03:59

1 Answers1

6
  1. Is the code perfectly legal?

No. It's undefined behavior. A compiler may choose to implement offsetof in that manner, but that's because it is the implementation: it can choose how to implement its own features. You, on the other hand, do not get such "luxury."

There is no way for you to implement the offsetof macro. Not in any standards-conforming manner.

  1. If I remove the final cast to (void*) from the macro definition, I get a segfault. Why? Shouldn't &(((type*)nullptr)->member) be a pointer of type type*, or is the type somehow erased here?

It's probably a segfault from trying to print my_offsetof(S, x) (since x is a char and that expression results in char*), because std::ostream's operator<< will try to print char* as a C-style string.

Community
  • 1
  • 1
Cornstalks
  • 37,137
  • 18
  • 79
  • 144
  • Not sure I understand "A compiler may choose to implement offsetof in that manner, but that's because it *is* the implementation." You mean that the compiler internally has a well defined behaviour in this case, by definition? – vsoftco Jan 27 '16 at 03:54
  • @vsoftco: I mean the compiler needs to provide some `offsetof` macro. However, *how* it does that is up to the compiler. If they choose to, they can make `&(((type*)nullptr)->member)` well-defined for their `offsetof` macro. But that doesn't make it well-defined for your own macro. – Cornstalks Jan 27 '16 at 03:58
  • @Cornstalks, in your last sentence s/probably// :) – rici Jan 27 '16 at 03:58
  • Thanks, that's what I understood from your answer too. Ohh I see what happens with the `char` also... For some reasons I thought the type is `S*`, but the type is the pointer to the member, so it will break whenever the member is a `char` due to invoking the `char*` overload. +1 – vsoftco Jan 27 '16 at 04:00
  • @vsoftco What you're doing might not work on all compilers. But it might work on, say, GCC 5. If it works on GCC but not MSVC, then MSVC will provide their own macro that's different and does work on MSVC. If you use your own version, then you won't know to change it for MSVC, and your code won't work on MSVC. – user253751 Jan 27 '16 at 04:06
  • @immibis Yeah I see... that's why I was curious if the code is "legit", as it looked fishy from the very beginning. I would definitely add a note on cppreference, as a reader may understand that "possible implementation" means "possible **standard-compliant** implementation". – vsoftco Jan 27 '16 at 04:07