Search code examples
c++intrusive-containersstandard-layout

Does a class need to be a standard layout type to be sure of the memory offsets of its members?


Lets say I want to write an intrusive list. I have an intrusive list class template that takes the type and a pointer to the member to use as the node. It looks roughly like this:

// This needs to be a member of anything the intrusive list is going to store.
class IntrusiveListNode {
    // ...
}

// The intrusive list itself
template <class T, IntrusiveListNode T::*Member>
class IntrusiveList {
    // ...
};

// This is a type that is going to be stored in an intrusive list.
class IntegerListNode {
public:
    IntrusiveListNode node;

private:
    int value;
};

// An example of the how the list would be used.
IntrusiveList<IntegerListNode, &IntegerListNode::node> myList;

Each thing you want to store in the list has an IntrusiveListNode on it. To turn that IntrusiveListNode back into a thing you can use, like an IntegerListNode, you call a function which does some pointer arithmetic on the node based on it's offset in the class, then casts it to the appropriate type. This seems to work, but I think it's not guaranteed to.

I'd like to be able to add a static_assert to my class that verifies at compile time that the type you're using is safe, but I'm not sure what the static_assert's condition would be. I think this is only guaranteed to work if the type holding the IntrusiveListNode is a standard layout class, but I'm not sure since the requirements for standard layout types seem stricter than I actually need.

In particular, a standard layout type requires that all members have the same access control. What I need is to just be able to make sure pointer arithmetic will work. That would mean you couldn't use this on a polymorphic type because two different versions of the struct could be laid out differently, but this shouldn't be an issue if the type has some mix of private and public data members, right? Would it be safe if I just required that the type be non-polymorphic? Or is there a better check to do? Or am I stuck doing an is_standard_layout check?


Solution

  • I cannot quote the standard on this (and I am pretty sure this is undefined-behavior-land), but you can generally rely on the consistency of an offset of a data member relative to a pointer to the containing class, provided that the static type of this pointer is constant (IntegerListNode* in your case).

    You may find that the offset changes when measuring it relative to a pointer to a derived type, but as long as you always reinterpret_cast first to IntegerListNode and only then static/dynamic cast to a derived type if desired, I would almost feel comfortable with it :)

    That is not to say this is a good idea. Such pointer arithmetic isn't required to implement an intrusive list. If you simply define a "next" and/or "previous" pointer in IntegerListNode (pointing to an IntegerListNode) you can pass a pointer-to-member for that to IntrusiveList and there will never be a need for creative conversions :)