blob: 39a143c35a65a705ecc14eb7803f699aef65cfb0 [file] [log] [blame] [view]
# Controlling container membership
As noted in the introduction, for objects to exist in an intrusive container,
users must explicitly add storage for the container bookkeeping to the object
itself. This section will show you the details of how you can control which
container(s) your object are allowed to exist in. It will:
1. Demonstrate the simple case of an object that may exist in a single
container.
1. Show two ways to allow for membership in multiple containers simultaneously.
1. Show how to take complete manual control of bookkeeping storage in your
object in the case that advanced requirements need to be satisfied.
## Single container membership using a mix-in.
Typically, choosing to use the default mix-in for a container is the easiest and
proper choice. You have already seen what this looks like for the doubly linked
list in the [Getting Started](getting_started.md) section of this guide. Here
are simple examples of all the default mix-ins.
```cpp
class FooObj : public fbl::SinglyLinkedListable<FooObj*> { /* ... */ };
using StackOfFoos = fbl::SinglyLinkedList<FooObj*>;
class FooObj : public fbl::DoublyLinkedListable<FooObj*> { /* ... */ };
using QueueOfFoos = fbl::DoublyLinkedList<FooObj*>;
class FooObj : public fbl::WAVLTreeContainable<FooObj*> { /* ... */ };
using MapOfIntToFoos = fbl::WAVLTree<int, FooObj*>;
// Hash tables default to singly linked list buckets
class FooObj : public fbl::SinglyLinkedListable<FooObj*> { /* ... */ };
using SLLHashOfIntToFoos = fbl::HashTable<int, FooObj*>;
// But you can use a doubly linked list as well.
class FooObj : public fbl::DoublyLinkedListable<FooObj*> { /* ... */ };
using DLLHashOfIntToFoos = fbl::HashTable<int, FooObj*,
fbl::DoublyLinkedList<FooObj*>>;
```
In each of these examples, a raw pointer to a `FooObj` was used. If you manage
your object using either `std::unique_ptr` or `fbl::RefPtr` semantics, they may
be substituted as needed provided that the pointer type in the mix in matches
the pointer type of the container.
Each of these objects can exist in a single _type_ of a container, but this does
not bind them to a single _instance_ of a container. For example:
```cpp
class Message : public fbl::DoublyLinkedListable<std::unique_ptr<Message>> { /* ... */ };
class TransmitQueue {
public:
// ...
void SendMessage(Payload payload) {
// Get a free message to send, or allocate if there are no free messages.
std::unique_ptr<Message> tx;
if (fbl::AutoLock lock(&lock_); free_messages_.is_empty()) {
tx = std::make_unique<Message>();
} else {
tx = free_messages_.pop_front();
}
tx.PrepareMessage(std::move(payload));
{
fbl::AutoLock lock(&lock_);
tx_pending_messages_.push_back(std::move(tx));
}
SignalTxThread();
}
// ...
private:
fbl::Mutex lock_;
fbl::DoublyLinkedList<std::unique_ptr<Message>> free_messages_ TA_GUARDED(lock_);
fbl::DoublyLinkedList<std::unique_ptr<Message>> tx_pending_messages_ TA_GUARDED(lock_);
};
```
`Message` objects in this example can exist in any instance of a
`DoublyLinkedList` of unique pointers to `Messages`, _but only in one of the
instances at a time_. In this example, a free-list of messages is maintained.
When the time comes to send a message:
1. A message is removed from the free list, or a new message is allocated if the
free list is empty.
2. The payload of the message is prepared.
3. The message is placed into the pending queue.
4. Finally, the worker thread is poked to tell it that it has messages waiting
in the pending queue to process.
When the worker is finished, it will move the `Message` object back to the free
list where it may be re-used, however that code has not been shown here.
## Multiple container membership using a multiple mix-ins
What if an object needs to exist in multiple containers at the same time? If
the container types themselves are different, then it is possible to simply use
multiple default mix-ins. Simply add the mix-ins to the list of base classes
for your object. For example:
```cpp
class FooObj : public fbl::RefCounted<FooObj>,
public fbl::DoublyLinkedListable<fbl::RefPtr<FooObj>>,
public fbl::WAVLTreeContainable<fbl::RefPtr<FooObj>> { /* ... */ };
using UniqueId = uint64_t;
static fbl::WAVLTree<UniqueId, fbl::RefPtr<FooObj>> g_active_foos;
static fbl::DoublyLinkedList<fbl::RefPtr<FooObj>> g_process_pending;
zx_status_t ProcessFooWithId(UniqueId id) {
if (auto iter = g_active_foos.find(unique_id); iter.IsValid()) {
if ((*iter).DoublyLinkedListable<fbl::RefPtr<FooObj>>::InContainer()) {
return ZX_ERR_BAD_STATE;
}
g_process_pending.push_back(iter.CopyPointer());
PokeWorkerThread();
} else {
return ZX_ERR_NOT_FOUND;
}
return ZX_OK;
}
```
In this example, a tree of all the active `FooObj`s in the system is being
maintained. The objects in the tree are indexed by their `UniqueId` (which is
just a big integer in this case). There is also a queue of `FooObj`s waiting
to be processed. The `ProcessFooWithId` function attempts to find the Foo with
the specified ID and put a reference to it in the `g_process_pending` queue.
Note that when an object is found in the set of active objects, it is checked to
make sure it is not _already_ in the pending queue before attempting to append
it to the pending queue. `FooObj`s can exist in both the pending queue and the
active set at the same time, but they cannot exist in the pending queue twice.
Attempting to put an object into an instance of `ContainerTypeA` when the
object is _already_ in an instance of `ContainerTypeA` (either the same instance
or a different one) will trigger a ZX_DEBUG_ASSERT if asserts are enabled, or
end up corrupting the program state otherwise. It is *very important* to make
sure that this does not happen. Frequently, invariants in a program ensure that
this can never happen, but if your program lacks such invariants, remember to
check your object to see if it is in a container already or not.
See the section on [testing for container membership](membership_tests.md)
for various ways this can be done. Also note how ugly testing for membership is
in this example. There are nicer ways to do this as you will see in the next
sub-section describing the use of `ContainableBaseClasses`.
One last thing to point out about this example. When it is time to put a
`FooObj` into the pending queue, a new instance of a `fbl::RefPtr` to the object
instance needs to be provided to `push_back`. This can be obtained by calling
the `CopyPointer` method of the iterator, which will invoke the copy constructor
of the underlying pointer type, giving us a new reference to the object.
For raw pointers, this is a no-op. For unique_ptrs, this is illegal
and will fail to compile.
## Multiple container membership using ContainableBaseClasses
What should you do if your object needs to exist in multiple containers of the
same fundamental type at the same time? The easiest thing that can be done is to
make use of `fbl::ContainableBaseClasses`, along with type tags, which can be
used to identify the different containers your object can exist in. Here is a
re-implementation of the previous example, but this time with the addition of
another list that the objects can exist in.
```cpp
struct ActiveTag {};
struct ProcessPendingTag {};
struct OtherListTag {};
class FooObj :
public fbl::RefCounted<FooObj>,
public fbl::ContainableBaseClasses<
fbl::TaggedDoublyLinkedListable<fbl::RefPtr<FooObj>, ProcessPendingTag>,
fbl::TaggedDoublyLinkedListable<fbl::RefPtr<FooObj>, OtherListTag>,
fbl::TaggedWAVLTreeContainable<fbl::RefPtr<FooObj>, ActiveTag>> { /* ... */ };
using UniqueId = uint64_t;
static fbl::TaggedWAVLTree<UniqueId, fbl::RefPtr<FooObj>, ActiveTag> g_active_foos;
static fbl::TaggedDoublyLinkedList<fbl::RefPtr<FooObj>, OtherListTag> g_process_pending_foos;
zx_status_t ProcessFooWithId(UniqueId id) {
if (auto iter = g_active_foos.find(unique_id); iter.IsValid()) {
if (fbl::InContainer<ProcessPendingTag>(*iter)) {
return ZX_ERR_BAD_STATE;
}
iter->SetPriority(fbl::InContainer<OtherTag>(*iter) ? 20 : 10);
g_process_pending_foos.push_back(iter.CopyPointer());
} else {
return ZX_ERR_NOT_FOUND;
}
}
```
The example starts by defining 3 different types ("tags") that will be used to
identify the different containers to be used concurrently with `FooObj`s. These
types don't actually _do_ anything, they are simply empty structures. You will
never instantiate any of them. Their purpose is only to be a unique type
that the compiler can use to understand which list type is paired with which
node state. In this example, the node state held by the
`TaggedDoublyLinkedListable` with the `ProcessPendingTag` is the node state used
by the `g_process_pending_foos` list.
Note that this can make the `InContainer` test easier to read as well. Using tags
allows us to invoke the stand-alone `fbl::InContainer<>` function, passing a
`const&` to an object, and specifying which type of container the object should
be tested for membership in using the tag.
`ContainableBaseClasses` works with any combination of containable mix-ins and
allows objects to exist in an arbitrary number of container types, provided that
each container type has a unique type to serve as its tag.
## Avoid mixing ContainableBaseClasses with default mix-ins
While _technically_ `ContainableBaseClasses` can be used in combination with the
default mix-ins, doing so is not considered best practice and should be avoided.
While there is clearly some extra typing involved in starting to use tags and
`ContainableBaseClases` to manage your object's container membership, once you
have started to do so it is easy to extend the pattern. The consistency of
always using tags with an given object (vs. sometimes using them and sometimes
not) will help with both readability and maintainability, particularly when it
comes to testing for container membership, and when understanding which
container type definitions use which piece of node storage in an object.
So, don't do this:
```cpp
namespace FooTags {
struct SortByBase {};
struct SortBySize {};
}
class Foo :
public DoublyLinkedListable<Foo*>, // For the pending processing queue
public fbl::ContainableBaseClasses<
public TaggedWAVLTreeContainable<Foo*, FooTags::SortByBase>,
public TaggedWAVLTreeContainable<Foo*, FooTags::SortBySize>> { /* ... */ };
```
Instead do something more like this:
```cpp
namespace FooTags {
struct PendingProcessing {};
struct SortByBase {};
struct SortBySize {};
}
class Foo :
public fbl::ContainableBaseClasses<
public TaggedDoublyLinkedListable<Foo*, FooTags::PendingProcessing>,
public TaggedWAVLTreeContainable<Foo*, FooTags::SortByBase>,
public TaggedWAVLTreeContainable<Foo*, FooTags::SortBySize>> { /* ... */ };
```
## Container membership using explicit nodes and custom traits
Finally, there is one last option for controlling container membership for objects.
This option is the lowest level option, and the most work to write, understand,
and maintain. It only should be used in situations where specific technical
requirements force you to do so. Here are some of the reasons that might
justify the use of explicit nodes and custom traits in order to control
container membership for your object.
* Your object must have a C++ standard layout and therefore cannot inherit from
any of the mix-ins.
* You must have precise control of _where_ in your object the node storage
exists, and cannot have it end up in the storage allocated by the compiler for
base classes.
* Your object is part of a complicated class hierarchy, where different levels of
the hierarchy each need to be containable in different containers. Use of the
mix-in helpers at the various levels would produce ambiguity and conflicts
because of inheritance.
Every basic container type has a `NodeState` type associated with it. Not
surprisingly, their names are:
* `SinglyLinkedListNodeState<PtrType>`
* `DoublyLinkedListNodeState<PtrType>`
* `WAVLTreeNodeState<PtrType>`
These are the structures that hold the actual bookkeeping used by the
container's data structure. In order to make use of them, you will need to:
1. [Add the appropriate instances of the node state types to your object.](#adding-node-state)
2. [Define a trait class, which will be used by containers in order access the
bookkeeping.](#defining-node-state-trait-accessors)
3. [Define a container type, specifying the appropriate trait class to link the
container type to the bookkeeping in your class it is supposed to make use
of.](#defining-container-types)
Here is an example of an object that can exist in two doubly linked lists using
explicit nodes and custom traits:
```cpp
class Obj {
public:
// Obj impl here
private:
struct FooListTraits {
static auto& node_state(Obj& obj) {
return obj.foo_list_node_;
}
};
struct BarListTraits {
static auto& node_state(Obj& obj) {
return obj.bar_list_node_;
}
};
friend struct FooListTraits;
friend struct BarListTraits;
fbl::DoublyLinkedListNodeState<Obj*> foo_list_node_;
fbl::DoublyLinkedListNodeState<fbl::RefPtr<Obj>> bar_list_node_;
public:
using FooList = fbl::DoublyLinkedListCustomTraits<Obj*, FooListTraits>;
using BarList = fbl::DoublyLinkedListCustomTraits<fbl::RefPtr<Obj>, BarListTraits>;
};
```
### Adding the node state bookkeeping {#adding-node-state}
These lines declare the storage required for `Obj` to exist in two different
doubly linked lists at the same time.
```cpp
fbl::DoublyLinkedListNodeState<Obj*> foo_list_node_;
fbl::DoublyLinkedListNodeState<fbl::RefPtr<Obj>> bar_list_node_;
```
The pointer type used for tracking needs to be specified for the node and needs
to match that of the container. In this example, `foo_list_node_` is a node
state object that can be used by lists to track their objects using raw
pointers, while `bar_list_node_` is a node state object that can be used by
lists to track their objects using `fbl::RefPtr<>`s. It is best practice to
make these node state objects private members of your class.
### Defining node state trait classes {#defining-node-state-trait-accessors}
These lines declare two "trait" classes used to tell a container type
how to access their associated node bookkeeping.
```cpp
struct FooListTraits {
static auto& node_state(Obj& obj) {
return obj.foo_list_node_;
}
};
struct BarListTraits {
static auto& node_state(Obj& obj) {
return obj.bar_list_node_;
}
};
```
These classes have no member variable or methods, merely a single static method
named `node_state`, which takes a mutable reference to your object type, and
returns a mutable reference to the proper node state bookkeeping instance in
your object. These classes will never be instantiated, they only are used to
define, at compile time, the relationship of a type of a container to the proper
unit of bookkeeping storage in the object to be contained.
Note that, in keeping with best practice, our node state instances are private
members of `Obj`. It is also best practice to make these trait classes private,
but because of the private nature of the node instances, you will need to also
declare the trait classes as friends of your object. In this example, these
lines take care of that task.
```cpp
friend struct FooListTraits;
friend struct BarListTraits;
```
### Defining container type and specifying the node state storage they should use {#defining-container-types}
Finally, you will need to define the container types that may be used with your
object and make those types available to the users of your object. In this
example, these are the lines that take care of that task.
```cpp
public:
using FooList = fbl::DoublyLinkedListCustomTraits<Obj*, FooListTraits>;
using BarList = fbl::DoublyLinkedListCustomTraits<fbl::RefPtr<Obj>, BarListTraits>;
```
Note that we have used one of the specialized `using` aliases for
`DoublyLinkedList`, specifically `DoublyLinkedListCustomTraits`. This alias
simple re-arranges the ordering of template parameters so that the second
parameter passed to the list type defines the trait class that will be used by
the list to find the appropriate node state bookkeeping storage.
Both the trait classes and the node state storage are private members of `Obj`,
so it is important that there be a public definition of the container types
available to the user of the object. This will allow them to say things like
the following.
```cpp
Obj obj_instance;
Obj::FooList list;
list.push_back(&obj_instance);
```