3

What I am trying to do is to have a class which is aware of its offset within an enclosing class with no runtime overhead at all. Here's an example of what I wish I could do:

template<int offset>
struct Inner {
};

struct Outer {
   int placeholder;
   Inner<offsetof(Outer, ptr)> ptr; 
};

The above code doesn't compile because offsetof(Outer, ptr) doesn't know about ptr (it's helping define it). I have implemented a few versions of this same idea that do incur runtime overheads (both in memory and executed instructions), but I'm having trouble implementing a "0 runtime overhead" version like my dream implementation above. Any ideas how this can be done?

Jarod42
  • 203,559
  • 14
  • 181
  • 302
antonin_scalia
  • 1,073
  • 2
  • 10
  • 11
  • 2
    You can't know what the layout of the class is until it is complete so I'm not sure there is anything you can do that would have zero run time cost. Not ever sure if reflection would help once we get it. What is the use case for this? – NathanOliver Dec 17 '19 at 17:15
  • I'm using this for a specific way of tracking accesses in computer architecture research, so other approaches won't work in context. While yes, the layout can't be known until the class is complete, I'm sure that there is some compile time workaround for this - perhaps some sort of wrapper class or something. I just can't think of it :( – antonin_scalia Dec 17 '19 at 17:28
  • If the size of `Inner` is fixed, there isn't really a fundamental reason why the layout of `Outer` cannot be calculated. I agree that my dream method won't work, but I'm really hoping for some sort of workaround because I don't think there's anything *actually* stopping this from working (somehow). – antonin_scalia Dec 17 '19 at 17:38
  • *"If the size of `Inner` is fixed"*. Hard to know in general case (with possible specializations). – Jarod42 Dec 17 '19 at 19:43
  • @antonin_scalia Inner's size doesn't matter. Its alignment does. If you can guarantee a fixed alignment for `Inner`, e.g., with `alignas(X)`, then its offset within `Outer` is fixed regardless of what might be inside of `Inner` (at least practically; theoretically, compilers may insert padding as they wish, but practically they only do it to align things). – Petr Skocik Dec 17 '19 at 20:00

2 Answers2

1
  1. Only the outer type can know the offset to the inner member sub-object
  2. Therefore the member sub-object type needs to know about the outer type
  3. Luckily, this sort of thing comes up fairly frequently. Indeed, the frequency with which it recurs could be considered ... curious.
  4. Unfortunately, no-one has come up with a good name for it. So, it's called the Curiously Recurring Template Pattern or CRTP for short.

The hard thing is figuring out a way for the outer type to automate production of the inner offsets, without having to write each one by hand. Doing it by hand is easy, but tedious, eg.

// use an enum to create distinct types
template<typename Outer, typename Outer::FieldId ID>
struct Inner
{
    static constexpr size_t offset();
};

struct Outer
{
    enum FieldId { First, Second };

    int header;
    Inner<Outer, FieldId::First> first;
    double interstitial;
    Inner<Outer, FieldId::Second> second;

    static constexpr size_t offset_of(std::integral_constant<FieldId, FieldId::First>) { return offsetof(Outer, first); }
    static constexpr size_t offset_of(std::integral_constant<FieldId, FieldId::Second>) { return offsetof(Outer, second); }
};

template<typename Outer, typename Outer::FieldId ID>
constexpr size_t Inner<Outer, ID>::offset()
{
    return Outer::offset_of(std::integral_constant<decltype(ID), ID> {});
}

This is clunky, partly because of the std::integral_constant wrapper (which could be avoided or typedef'd), but mostly because the ID-to-field mapping has to be expressed manually in code.

Automating production is hard without compile-time reflection. You can automate everything if you just use a tuple-like object instead of a struct at the top level, but that makes it harder to interleave "smart" and dumb members, and probably changes the layout, and it definitely breaks the StandardLayoutType requirements, which may prevent offsetof from working entirely.

Useless
  • 64,155
  • 6
  • 88
  • 132
0

It could be simulated ugly way with the code duplication and a lot of boilerplate asserts as: declare a 'template' class with the same layout, declare target class, do a compile time assert that corresponding fields have the same offsets.

struct OuterTemplate {
   int placeholder;
   Inner<0> ptr; 
};

struct Outer {
   int placeholder;
   Inner<offsetof(OuterTemplate, ptr)> ptr; 
};

static_assert(offsetof(OuterTemplate, ptr) == offsetof(Outer, ptr), 
              "Same offsetof assumption is not working");
Renat
  • 7,718
  • 2
  • 20
  • 34