7
class Base1 {
    int x;
};
class Base2 {
   int y;
};
class Derive : public Base1, public Base2 {
public:
    enum {
        PTR_OFFSET = ((int) (Base2*)(Derive*)1) - 1,
    };
};

But the compiler complains

expected constant expression

Everyone knows that the expression values 4 except the compiler, what goes wrong?

How, then, to get the offset at compile time?

etnlGD
  • 85
  • 6
  • There is no pointer address in compile-time. – masoud Sep 28 '13 at 09:05
  • It compiles on my machine (looks like GNU extension), but smells like UB, and PTR_OFFSET will be 0. What is it that you're trying to achieve? – jrok Sep 28 '13 at 09:11
  • @MM.Why can't my approach work? – etnlGD Sep 28 '13 at 09:13
  • 1
    @MM. Except for function pointers and pointer to objects with static storage duration. – jrok Sep 28 '13 at 09:14
  • @jrok The PTR_OFFSET should be 4, I have verified this by runtime output in MSVC. – etnlGD Sep 28 '13 at 09:21
  • @jrok Only if the expression is 0, can Derive** be cast to Base** safely. So I need the value at compile time. – etnlGD Sep 28 '13 at 09:27
  • 1
    The code is rejected because a `reinterpret_cast` is not allowed in a constant expression (C++11) or because you may not cast to non-integral, non-enumeration types (C++03). The `reinterpret_cast` here is disguised as a C-style cast: `(Derive*)1` – dyp Sep 28 '13 at 11:10
  • In what scenario do you need to cast `Derive**` to `Base**` ? – willj Sep 28 '13 at 14:57

3 Answers3

5

Addressing the immediate compiler error you are seeing in the supplied code, (Base2*)(Derive*)1 will most likely become reinterpret_casts when compiled, and that as DyP wrote as a comment to the question is not a constant expression which is required for enumeration initialization. Some compilers, notably GCC are not as strict on this point and will allow for reinterpret_cast in constant expressions even though it is forbidden by the standard (for further discussion of this see the comments for this GCC bug http://gcc.gnu.org/bugzilla/show_bug.cgi?id=49171 and Constexpr pointer value).

The broader question of identifying at compile time what the layout of an object is and the offsets to its various members is a tricky one without a well-defined answer. The standard give implementers a lot of latitude to pad/pack an object's fields, usually out of alignment considerations (for a good summary see http://www.altdevblogaday.com/2013/05/03/cc-low-level-curriculum-part-11-inheritance/). While the relative ordering of an object's field must be maintained, int y in an instance of Derived need not be at an offset of sizeof(x) from the start of the instance of Derived; the answer is compiler and target architecture dependent.

All of that being said, this sort of structure layout information is determined at compile time and at least on some compilers is made accessible (even if not in portable, standards compliant ways). In the answer to this question, C++ Compile-Time offsetof inside a template, Jesse Good provides some code that on GCC at least will allow one to determine field offsets within a type at compile time. This code will unfortunately not provide the correct offsets for base class members.

A good solution to your problem awaits implementation of compile time reflection support in C++, something for which there is ongoing work as part of a standards working group: https://groups.google.com/a/isocpp.org/forum/#!forum/reflection .

Community
  • 1
  • 1
Sean Boocock
  • 131
  • 1
  • 5
2

Here is an example that works on Clang.

The approach uses a builtin that is available on both Clang and GCC. I have verified Clang (see Code Explorer link below), but I have not attempted with GCC.

#include <iostream>

/* Byte offsets are numbered here without accounting for padding (will not be correct). */
struct A { uint64_t byte_0, byte_8; uint32_t byte_16; };
struct B { uint16_t byte_20, byte_24; uint8_t byte28; uint64_t byte_29; };
struct C { uint32_t byte_37; uint8_t byte_41; };
struct D { uint64_t byte_42; };
struct E : A, B, C, D {};

template<typename Type, typename Base> constexpr const uintmax_t
offsetByStaticCast() {
  constexpr const Type* Type_this = __builtin_constant_p( reinterpret_cast<const Type*>(0x1) )
                                  ? reinterpret_cast<const Type*>(0x1)
                                  : reinterpret_cast<const Type*>(0x1);

  constexpr const Base* Base_this = static_cast<const Base*>( Type_this );

  constexpr const uint8_t* Type_this_bytes = __builtin_constant_p( reinterpret_cast<const uint8_t*>(Type_this) )
                                           ? reinterpret_cast<const uint8_t*>(Type_this)
                                           : reinterpret_cast<const uint8_t*>(Type_this);

  constexpr const uint8_t* Base_this_bytes = __builtin_constant_p( reinterpret_cast<const uint8_t*>(Base_this) )
                                           ? reinterpret_cast<const uint8_t*>(Base_this)
                                           : reinterpret_cast<const uint8_t*>(Base_this);

  constexpr const uintmax_t Base_offset = Base_this_bytes - Type_this_bytes;
  
  return Base_offset;
}

int main()
{
  std::cout << "Size of A: " << sizeof(A) << std::endl;
  std::cout << "Size of B: " << sizeof(B) << std::endl;
  std::cout << "Size of C: " << sizeof(C) << std::endl;
  std::cout << "Size of D: " << sizeof(D) << std::endl;
  std::cout << "Size of E: " << sizeof(E) << std::endl;

  /* Actual byte offsets account for padding. */
  std::cout << "A offset via offsetByStaticCast<E, A>(): " << offsetByStaticCast<E, A>() << std::endl;
  std::cout << "B offset via offsetByStaticCast<E, B>(): " << offsetByStaticCast<E, B>() << std::endl;
  std::cout << "C offset via offsetByStaticCast<E, C>(): " << offsetByStaticCast<E, C>() << std::endl;
  std::cout << "D offset via offsetByStaticCast<E, D>(): " << offsetByStaticCast<E, D>() << std::endl;

  return 0;
}

Output:

Size of A: 24
Size of B: 16
Size of C: 8
Size of D: 8
Size of E: 56
A offset via offsetByStaticCast<E, A>(): 0
B offset via offsetByStaticCast<E, B>(): 24
C offset via offsetByStaticCast<E, C>(): 40
D offset via offsetByStaticCast<E, D>(): 48
Program ended with exit code: 0

Code available on Compiler Explorer: https://godbolt.org/z/Gfe6YK

Based on helpful comments from constexpr and initialization of a static const void pointer with reinterpret cast, which compiler is right?, particularly including the link to a corresponding LLVM commit http://lists.llvm.org/pipermail/cfe-commits/Week-of-Mon-20120130/052477.html

Asher
  • 1,195
  • 1
  • 11
  • 19
  • 1
    Devilishly clever. Interestingly, using `0x0` instead of `0x1` breaks the function, so I'm guessing the compiler understands we are working with `NULL` and tries to prevent us from casting `nullptr` to a sub or base class. Also, can be shortened with the help of `#define BUILTIN_CONSTEXPR(expr) (__builtin_constant_p((expr)) ? (expr) : (expr));` and underflow can be avoided by initializing `Type_this` with `sizeof(Type)` instead of `0x1`. – Pietro Saccardi Sep 23 '21 at 23:42
0

Recently I found that the code ((int) (Base2*)(Derive*)1) - 1 broke down when Derive : public virtual Base2. That is, the offset of virtual base is unknown at compile-time. Therefore, it it forbidden in c++ standard.

etnlGD
  • 85
  • 6