tags:

views:

1054

answers:

7

Given a variable foo of type FooClass* and a member variable in that class named bar, is the distance between foo and &(foo->bar) the same in any situation with some constraints:

  1. FooClass is a non-POD type.

  2. We know that foo will always point to an instance of FooClass, and not some subtype of it.

  3. We only care about behaviour under a single compiler and a single compilation; that is, the value this may result in under gcc is never used in code compiled with MSVC, and it is never saved to be re-used between compilations. It is computed in the binary and used in the binary, and that is it.

  4. We don't use a custom new, although some instances of the class may be stack-allocated and some heap-allocated.

  5. There is no explicit ctor for FooClass; it relies upon the compiler-generated one (and each of the fields in FooClass is either POD or default-constructable).

I can't find a guarantee either way on this in the standard (nor did I expect to), but my rudimentary testing with gcc leads me to believe that it will always be the case there. I also know that this guarantee is made for POD-types, but let us assume this type can't be POD.

An update/clarification: this is just for a single compilation of a single binary; the calculated offsets will never leave that single execution. Basically, I want to be able to uniquely identify the fields of a class in a static map and then be able to lookup into that map for some macro/template/EVIL trickery. It is merely for my own amusement, and no life support machines will rely on this code.

+3  A: 

I'm no expert but i gonna try answering you anyway :)

  1. FooClass is a non-POD type. This mean it could have more than one sections of private, public or protected. Within such a section, the order is that of the definition of the members, but across those sections, order is arbitrary and unspecified.
  2. foo will always point to FooClass. Well so we have guarantee there is no offset adjustment done. At least in one compilation, offsets will be the same then (don't have the backing up Standard quote. But it can't work if they were different).
  3. We only care about behavior on a single compiler. Well since the order of members is unspecified across sections of access modifiers and the compiler is allowed to put padding between members, this won't buy us much.
  4. We only care about objects on the stack (automatic storage duration). Well i don't see how that changes anything of the object layout.

So after all i don't think you have any guarantee that the offset will be constant across compilations. For considerations within one compilation (so if we would play with a compiler whose generated code uses an ABI that changes with each different compilation), the offset just can't be different. But even if you know the offset, you can't access the member. Your only way to access a member is using the member access operator -> and . (that's said in 9.2/9).

Why not use data member pointers? They allow accessing members safely. Here is an example: (looking up members by name).

Johannes Schaub - litb
I want to uniquely identify members such that I can put them in a "set" of sorts and iterate over them. It involves templates and macros, so it needs to be automatic (I can't just do it in the ctor by name). My current hack works, but thanks for the informative post!
hazzen
you can sort by members using data member pointers too
Johannes Schaub - litb
I'm doing something similar (macro-based FooOptions structs to build Foo objects that would need many params to set up, some optional and some required), and I was trying for as few keystrokes as possible - the linked way would work as well if I wanted more typing.
hazzen
@litb: Interesting, I didn't realise that the compiler was obliged to order members by their declaration order within access-specifier sections.
j_random_hacker
And by the way I'd say you *are* an expert litb!
j_random_hacker
+1  A: 

There are two things off the top of my head that will affect the internal layout of an object:

  • the size of member objects. Hopefully you'll recompile all affected modules if you change a member definition.
  • packing pragmas may change the padding added between members. Be sure that the packing is the same for all modules that use the class, or at least the section where the class is defined. If you don't, you'll have bigger problems than unpredictable offsets.
Mark Ransom
If the member variable is a pointer, how does the size of the member object affect that? Surely the only thing that will matter is the ILP (integer, long, pointer) model for the compiler/platform that you are working on.
Rob Wells
I said member objects, not member pointers. If you stretch the definition of "object" to include POD types including pointers, the assertion still holds; as long as the pointer size (not the size of the pointed object) doesn't change, you're OK.
Mark Ransom
+1  A: 

Bottom line: If this class contains anything other than PODs, then you can make absolutely no assumptions about the offsets. If the class is just a collection of public PODs, then you're safe.

Here is a link to a portion of a chapter in an excellent intermediate C++ book. I recommend everyone to read this book if you're serious about C++.

This particular excerpt addresses a portion the question presented here:

http://my.safaribooksonline.com/0321321928/ch11?portal=oreilly

For the rest of the details, check out the book. My "bottom line" above is a simplistic summary of this chapter.

Marcin
+3  A: 

Under a single compiler where the compiler settings are always the same and there is nothing added to or taken away from FooClass, then yes, the distance between the address stored at foo and &(foo->bar) will always be the same, or the compiler wouldn't be able to generate proper code that worked across compilation units.

However, once you add anything to the class, or change the compiler settings, all bets are off.

Joel
+2  A: 

As far as I know, this should always be the case, POD class or not. At compile time, based on the compiler, architecture, settings, etc., the compiler determines the size of the class and the offsets of all its members. This is then fixed for all instances of the class in the compilation unit (and by extension the linked unit, if the one-definition rule is preserved).

Since the compiler treats type pointers literally, even if the underlying type is wrong (eg: the pointer has been c-style cast incorrectly), the computed distance between &foo and &(foo.bar) will be the same, since the offset is known statically at compile time.

Note: This has definitely been done before, effectively. See, for example, Microsoft's ATL data binding code using their 'offsetof' macro...

Nick
offsetoff is defined in the standard to only take POD types/unions (section 18.1)
hazzen
The macro works just as well for non POD types; I have used similar constructs. They are particularly useful for mapping data bindings for database access mechanisms (the same thing MS uses it for).
Nick
it doesn't work for non POD types. It's only for POD types.
Johannes Schaub - litb
+4  A: 

After you have compiled your program, Yes*.

The offset will remain constant.

There is one very important restriction, however: foo must be pointing specifically to a FooClass object. Not a class derived from FooClass, or anything else for that matter.

The reason that C++ makes the POD distinction regarding member offsets is because both multiple inheritance and the location (or lack of) a vtable pointer can create situations where the address of an object is not the same as the address of that object's base.

Shmoopty
+1  A: 

Yes, the offset is determined at compile-time, so as long as you're not comparing offsets across compilations or compilers, it will always be constant.

jalf