Pardon my...
Implement ListHook and IntrusiveList<T, Hook>: an intrusive doubly-linked list where the link pointers live inside the elements (via an embedded hook) rather than in nodes the list allocates. The list owns nothing and allocates nothing — it threads through objects whose storage is owned elsewhere (here, a pool provided by the grader).
This problem is designed to require careful handling of:
- the
container_ofidiom (recovering an element from a pointer to its embedded hook) - O(1) erase given only the element (no search, no position)
- multiple simultaneous list memberships via multiple hooks in one object
- auto-unlink: coupling an element's destruction to its removal from every list
An intrusive list stores no values of its own. Each element embeds one or more ListHook members; a list threads its ring through one specific hook. Because the hook lives in the element:
- linking/unlinking allocates nothing,
- an element can be removed in O(1) given only a reference to it,
- one element can be on several lists at once (one hook per list),
- destroying an element must splice it out of whatever list holds it.
#include <cstddef>
// Embed one ListHook per list-membership in your element type.
struct ListHook {
ListHook() noexcept; // starts unlinked
~ListHook(); // AUTO-UNLINK if still linked
ListHook(const ListHook&) = delete; // hooks are not copyable/movable
ListHook& operator=(const ListHook&) = delete;
bool linked() const noexcept; // true if currently in a list
void unlink() noexcept; // splice self out; become unlinked
// (internal link pointers are yours to define)
};
// Threads a ring through the hook identified by the pointer-to-member Hook.
// IntrusiveList<Order, &Order::book_hook> book;
template <class T, ListHook T::* Hook>
class IntrusiveList {
public:
IntrusiveList() noexcept; // empty
~IntrusiveList(); // unlinks all elements; destroys none
IntrusiveList(const IntrusiveList&) = delete;
IntrusiveList& operator=(const IntrusiveList&) = delete;
bool empty() const noexcept;
std::size_t size() const noexcept; // O(n) walk is acceptable
void push_back(T& x) noexcept; // link x at the tail
void push_front(T& x) noexcept; // link x at the head
static void unlink(T& x) noexcept; // O(1) remove x given only x
T& front() noexcept; // returns the element, by identity
T& back() noexcept;
void clear() noexcept; // unlink all; destroy none
template <class Fn>
void for_each(Fn fn); // call fn(T&) front-to-back
};The element type, its hook member(s), and the backing pool are provided by the grader. You implement only ListHook and IntrusiveList.
When walking the ring you hold a ListHook*, but callers want the enclosing T. Recover it by subtracting the hook's offset within T:
T* obj = reinterpret_cast<T*>(
reinterpret_cast<char*>(hook_ptr) - offset_of(Hook));front(), back(), and for_each must return / pass the actual element objects the caller linked — never copies. The grader checks pointer identity (&list.front() == &the_object_you_pushed) and that mutating an element is visible through the list.
- The list never allocates.
push_back,push_front,unlink,clear, and iteration must perform zero heap allocations. (The grader interposesoperator newand asserts the count does not move across a long run of link/unlink operations.) - The list never constructs or destroys elements. It borrows objects owned by the pool. Destroying the list (or calling
clear) unlinks elements but must not run their destructors. - A
ListHookis not copyable or movable: its address is baked into its neighbors' links, so copying would alias and moving would dangle.
When an element is destroyed while still linked, its hook's destructor must splice it out of the list, so that no dangling link into freed/reused storage survives.
Item* a = pool.create(...); Item* b = pool.create(...); Item* c = pool.create(...);
list.push_back(*a); list.push_back(*b); list.push_back(*c); // [a, b, c]
pool.destroy(b); // ends b's lifetime. NO unlink(b) call.
// ~ListHook must have removed b, leaving [a, c].The grader destroys a linked element without any manual unlink call, then immediately reuses that slot (the pool is address-stable and reclaims LIFO, so the new object occupies the freed element's exact address, overwriting its old hook bytes). An implementation that does not auto-unlink leaves a neighbor pointing into the reused slot; the ring is then corrupted and the subsequent walk is wrong. The grader compiles with AddressSanitizer, so a stale read of the freed hook is also caught directly.
IntrusiveList::~IntrusiveList() and clear() must likewise leave every element's hook in the unlinked state, so an element that outlives the list can be re-linked or destroyed cleanly afterward.
unlink(x)is O(1) and requires nothing butx. It is valid only whenxis currently linked into the list of the matching hook.for_each(fn)must toleratefncallingunlinkon the current element (cache the next pointer before invokingfn).- One element carrying two hooks may be on two lists simultaneously; unlinking it from one must not affect the other.
size()may be O(n).
push_back,push_front,unlink(x),front,back,empty: O(1).size,clear,for_each,~IntrusiveList: O(n).- All of the above: zero allocation.
Single-threaded. No threads, timing, randomness, or OS-specific behavior.
- Recommended internal design: a circular sentinel ring (a dummy hook the list owns as its anchor). An element hook is "unlinked" when its
next/prevpoint at itself. The sentinel makes every splice branchless — no special-casing the empty list or the ends. - The grader provides an address-stable pool (
create/destroy) and a tracked element type embedding two hooks. You do not implement the pool. - You may use
<cstddef>and<utility>. You may not usestd::list,std::vector,std::deque, or any standard container as the backing structure — the links must live in the elements. These identifiers are reserved and will fail to compile if used. - The grader compiles with
-fsanitize=address,undefined. Use-after-free from a missing auto-unlink, and dangling reads from a brokencontainer_of, are detected immediately.
Running against custom tests...