0

The question: Can the output of the below program be predicted reliably, given the full code for B and C?

If the answer is "no" (e.g. due to platform dependence), then: Is there a way to make it predictable (e.g., by using certain allocation/alignment techniques).

struct B{
    // ...
};

struct C{
    // ...
};

struct A{
    B b;
    C c;
};

int main(){
    A a;
    long db = (int*)(&a.b) - (int*)(&a  );
    long dc = (int*)(&a.c) - (int*)(&a.b);
    std::cout << "difference a.b to a   : " << db << "\n";
    std::cout << "difference a.c to a.b : " << dc << "\n";
}

Remark(s):

  • The use of int* is just because cpp disallows use of void* afaik.
  • I intend to eventually use db,dc to compute the address of b and c from a given object of A at another compile time.
pwtrzep
  • 3
  • 2
  • 2
    Why `int*` instead of `char*`? Can you explain a little more how you're planning to use this? Is the second line reversed or did you want a negative vale? – Retired Ninja Apr 11 '23 at 16:20
  • @RetiredNinja Thanks for the suggestion. Question accordingly improved. The details of how I want to use it are very lengthy and domain-specific. – pwtrzep Apr 11 '23 at 16:37
  • 1
    What is "reliably predictable"? Alignment requirements, padding and type sizes vary between platforms which can affect the results. Also &a.c may not be a legal int * value on some platforms depending on stuff such as what the definition of B is. In general I wouldn't consider void * or int * as appropriate here, but as @RetiredNinja seems to suggest, char *. – Avi Berger Apr 11 '23 at 16:37
  • @AviBerger Very intersting! Can you think of possible solutions? E.g., a policy that B and C each hold an integer aggregate themselves might help? Could a 32 bit alignment work? Would it help if A has only pointer objects (that would mean each object has size 64 bit)? – pwtrzep Apr 11 '23 at 16:41
  • 2
    The reason I asked about `int*` versus `char*` is they will give you different results. https://godbolt.org/z/4q5fGfjWx If we knew a little more about what you're trying to do we might be able to give better advice. You might take a look at `offsetof`. [Determining struct member byte-offsets at compile-time?](https://stackoverflow.com/questions/19943194/determining-struct-member-byte-offsets-at-compile-time) – Retired Ninja Apr 11 '23 at 16:41
  • 1
    If you need your structs to be of the same size and alignment across multiple platforms you certainly can do that if you design them with that in mind. By, for example, avoiding implementation defined types such as `int`s or pointers (different between 32 and 64bit platforms). – guard3 Apr 11 '23 at 16:44
  • @RetiredNinja The issue with offsetof and nameof and other macros is that there are two compile times. So, my application is a codegen for a derived struct of A that shall have extended routines. An original routine in A would call member-functions of B and C that write the actual codegen. They must be able to address themselves in the generated code. So if A::eval() calls B::eval(), then B::eval() will contain code that produces lines in extendedA.eval_adjoint(), in which b.eval_adjoint() is called. The "b.eval_adjoint()" is what the code-generator must produce, but the string "b" is unknown. – pwtrzep Apr 11 '23 at 16:50
  • @guard3 Actually I don't want B and C to have to be of same size. I understand my (not) proposed solution below implies this principle, but it is undesirable. Typically, B and C will be of different sizes. – pwtrzep Apr 11 '23 at 16:52
  • @pwtrzep I mean B and C being the same B and C across different platforms. – guard3 Apr 11 '23 at 16:57
  • @guard3 That is an interesting aspect of the question, actually. I do not know what code for B and C I would have to write in order for them to be the same B and C on different platforms. Is that what you mean? – pwtrzep Apr 11 '23 at 17:02
  • This seems to be a legitimate use case for member pointers. Is there any specific reason you don't use them?! – Red.Wave Apr 11 '23 at 17:18
  • Just a stab in the dark: have you considered something like `B::eval( std::basic_ostream &codeOutputStream, std::string myName );` and avoiding these address calculations altogether? – Avi Berger Apr 11 '23 at 17:23
  • @Red.Wave Do you mean B* pb; and then A::A(){ pb=new B;} ? That seems orthogonal to the question to me. In the pointer case, I would need to know the difference between the memory position of a.pb and a . – pwtrzep Apr 11 '23 at 17:24
  • `(int*)(&a.b) - (int*)(&a );` is undefined. you need to use [`offsetof`](https://en.cppreference.com/w/cpp/types/offsetof) to get that offset – 463035818_is_not_an_ai Apr 11 '23 at 17:30
  • No I mean `B A::*pb=&A::b; (void)(a.*pb);`. – Red.Wave Apr 11 '23 at 17:36

2 Answers2

0

One possible answer I could think of seems to impose the policy on A that all of its members must be pointers and stand in the same array.

struct A{
  int32_t members[2];
  A(){
    members[0] = (int32_t)(new B);
    members[1] = (int32_t)(new C);
  }
};

In that case the output as in the question is predictable. However, the code of A will be virtually unreadable. Thus, this would be a particularly bad solution.

Remark: As hinted by AviBerger, a type like int32_t should be used here to assert platform-independent pointer arithmetic.

pwtrzep
  • 3
  • 2
  • There are platforms with 16 bit pointers. Others with 32 bits. Still others with 64 bits. Could even be other sizes. This also introduces memory management issues. Better than pointer members would perhaps be a fixed width integer type like int32_t. – Avi Berger Apr 11 '23 at 16:55
  • @AviBerger Many thanks! I have seen this before in optimized code. Very important detail for this answer to work. I revised accordingly. (I have to little stats to upvote your comment). – pwtrzep Apr 11 '23 at 17:04
  • I meant a scalar type rather than a pointer type. Then again maybe you are only targeting platforms with 64 bit pointers. I don't know. – Avi Berger Apr 11 '23 at 17:07
  • @AviBerger Oh, I actually don't. I didn't know one can convert pointers into int32_t. – pwtrzep Apr 11 '23 at 17:22
  • Trying to convert pointers to an int32_t is a bad thing to try. I hadn't grasped that you specifically desire pointers here. It seems I've lead you astray. Forget a lot of what I said. Sorry. – Avi Berger Apr 11 '23 at 17:27
0

The answer is yes, provided that B and C have the same size and alignment across platforms. For example, a struct such as this:

struct meow {
    int a;
    void* b;
};

won't have the same size and alignment across multiple platforms. The above would (most likely) be of size 8 on a 32 bit platform (ints are usually 32 bit but that's not guaranteed) and of size 16 on a 64 bit platform (64bit pointer and 8 byte allignment). So, member offsets may be different.

In order to have a "predictable" offset for your members, be sure to avoid pointers (different size on each platform unless you target either 32 or 64 bit platforms exclusively), standard types such as int (which are implementation defined) and, of course, standard library types (implementation defined as well).

#include <cstdint>

struct meow { // size 28, alignment 4
    int32_t a;     // Offset 0, guaranteed to be 32 bits, alignment 4
    float   b;     // Offset 4, guaranteed to be 32 bits, alignment 4
    char    c[20]; // Offset 8, *usually* a char is 8 bits, alignment 1
};

The above is just a sample, you can adjust depending on your needs and the different platforms you want to accommodate.

Overall this is a good read: https://en.cppreference.com/w/c/language/object

guard3
  • 823
  • 4
  • 13