1

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?

Alex
  • 14,973
  • 13
  • 59
  • 94

1 Answers1

1

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 :)

heinrichj
  • 562
  • 2
  • 5
  • The main issue I have with having the next and previous pointers be pointers to T instead of pointers to the next node is that it makes adding a move constructor to the node itself impossible. The class with the node member must now manually call some utility function to update the pointers appropriately. It's not the worst thing in the world, but it's annoying to require that that the user do this extra work to include nodes in their struct or class. – Alex May 26 '15 at 18:14
  • How about making your utility class "IntrusiveListNode" a class template and have it contain typed pointers to whatever the element type is? Equivalent to direct pointer members except you only need to implement the list-related behavior once. Alternatively you could make IntrusiveListNode a base class (using CRTP). – heinrichj May 27 '15 at 05:53
  • Making it a template means losing a lot of features, like the ability to have it remove itself from the list (which is especially useful during destruction), and having it have a move constructor. Both are impossible because the node doesn't know where it is in the object that owns it, so it can't grab the next and previous pointers to get them updated. Making it a base class doesn't work too, because then you can't have multiple intrusive list nodes, which means you can't be in multiple lists, which is one of the nice features of an intrusive list. – Alex May 27 '15 at 19:59