This section will show you a simple example of an intrusive container in use, and spend a small amount of time discussing the specifics of some basic operations. While subsequent sections of the guide will go into greater detail on these concepts as well as a number of others, this section will give a basic demonstration of:
- Pointer types and how they are used to control objects' lifecycles.
- Containable "mix-ins" and how they are used to allow an object to exist in a container.
- How to iterator over the elements in a
fbl::
intrusive container. - How to remove elements from a
fbl::
intrusive container. - Setting up build dependencies from a
fbl::
intrusive container.
A simple example
Let's start with a simple example and go from there. In this example, an object
will be defined that can exist in a doubly linked list, tracked using
std::unique_ptr<>
. A list of these objects will be:
- Populated
- Iterated over to print a subset of the objects
- Iterated over to remove a subset of the objects
- Explicitly cleaned up
#include <stdint.h>
#include <fbl/intrusive_double_list.h>
// An object that holds an immutable int and can exist on a doubly linked list
// managed using a std::unique_ptr
class MyObject : public fbl::DoublyLinkedListable<std::unique_ptr<MyObject>> {
public:
explicit MyObject(int val) : val_(val) {}
int val() const { return val_; }
private:
const int val_;
};
extern void TakeThisObjectFromMe(std::unique_ptr<MyObject>);
void DoThings() {
fbl::DoublyLinkedList<std::unique_ptr<MyObject>> list;
// Add 100 random integers to our list.
for (uint32_t i = 0; i < 100; ++i) {
list.push_back(std::make_unique<MyObject>(rand()));
}
// print out any members of the list that are even
for (const auto& obj : list) {
if (!(obj.val() % 2)) {
printf("Even Object %d\n", obj.val());
}
}
// Remove any objects that are divisible by 7 and give them to someone else.
for (auto iter = list.begin(); iter != list.end(); ) {
auto consider = iter++;
if (!(consider->val() % 7)) {
TakeThisObjectFromMe(list.erase(consider));
}
}
// Destroy the rest of the object by forcing the list to release its unique
// references to the objects. We could also simply let the list leave the
// scope of the function, which would do the same.
list.clear();
}
Pointer types
One of the first things to note here, and an important difference between
std::
containers and fbl::
containers, is that fbl::
containers always
track objects using pointers. In particular, they are tracked using a set of
explicitly supported pointer types. When the type of a fbl::
list is defined,
it is defined as a list of a specific pointer type to an object type. In this
example, a std::unique_ptr<>
was chosen meaning that the unique reference to
the object may be held locally, or held by the list, but it cannot be held by
both at the same time. There are currently 3 different pointer types that
fbl::
containers allow:
T*
: A raw pointer type with no managed RAII semantics.std::unique_ptr<T, Deleter>
: The standard unique pointer type, either with a custom deleter, or with the default deleter.fbl::RefPtr<T>
: Afbl::
intrusive reference pointer. Basicallyfbl::
's intrusive version of astd::shared_ptr<T>
.
All containers must be told which type of object they hold and which type of
pointer is being used to manage lifetime via the first template argument of the
container type. When managed pointer types are used, containers always take
ownership of a reference when the object is added to the container, and give
back ownership of that reference when the object is removed from the container.
The exception to this rule is clear()
, which drops all references.
"Raw" pointers have no special ownership semantics, meaning that it is entirely up to the user to ensure that objects are kept alive while in a container, and that the are properly cleaned up when removed or cleared from a container.
Listable/Containable Mix-in classes
If the object must have the next/prev pointers stored in it somewhere, where
does this node state live and how does the list find it? There are actually
several ways to control this behavior, however this example demonstrates
simplest possible approach. This is to derive from the DoublyLinkedList
's
Listable
mix-in helper. Just like with the list itself, you need to inform the
mix-in of the object type as well as the pointer type via the first template
argument. These types need to match the types given to the list definition
itself. By default, a list will go looking for an instance of its Listable
mix-in that the object derives from in order to find the node state it uses for
bookkeeping. All of this happens at compile time, there is no runtime overhead
involved in locating the node state in the object.
Iteration
fbl::
containers support both ranged-based for loop iteration, as well as the
more classic begin()/end() style of iteration. As with std::
containers, when
enumerated using a range-based for, an l-value reference to the object will be
returned. It is important to remember to say either auto&
or const auto&
(or even MyObject&
) in the for loop and not simply auto
, lest you end up
accidentally triggering the copy constructor of your object.
fbl::
container iterators also support the standard pre-fix and post-fix forms
of the ++
operator, which you can see being used to optionally remove elements
from the list as you iterate. The post-fix form is used in this example so that
consider
becomes the element that may chosen for removal, while iter
becomes a pointer to the next element in the list that needs to be considered.
Iterators act like unmanaged pointers to the elements while they are in the
list. The underlying object may be accessed in all the standard ways, using
either the ->
operator, or using the (*iter).val()
style. A raw pointer can
even be fetched to the object by saying &(*iter)
. Care should be taken with
iterators as they are functionally raw pointers to an object. It is not advised
that you be store them for any length of time, especially when you are using modern
managed pointers to control the lifecycles of your objects.
Removing elements
In this example, when elements are removed, they are removed using iterator based erase. The result of the erase operation will be an r-value reference to the pointer type used by the list. The reference could have been taken back and held in a local pointer instance, but in this case it was moved directly into the call of some external function transferring the unique ownership of the object from the list to the external function in the process.
clear()
is another way to removes elements from a container, but it will drop
all of the element references, potentially destroying the objects they point to
in the process.
Setting up build dependencies
In order to make use of fbl::
containers, users must be sure to both include
the proper header files for the containers they want to use, and to include a
dependency on the fbl
library in their project's BUILD.gn
file.
The required include files for each container type are:
Container type | Include statement |
---|---|
fbl::SinglyLinkedList<> |
#include <fbl/intrusive_single_list.h> |
fbl::DoublyLinkedList<> |
#include <fbl/intrusive_double_list.h> |
fbl::WAVLTree<> |
#include <fbl/intrusive_wavl_tree.h> |
fbl::HashTable<> |
#include <fbl/intrusive_hash_table.h> |
Users also must add the library //zircon/system/ulib/fbl
to either the deps
or public_deps
section of their project's BUILD.gn
file. The reference
belongs in the deps
section in the case that the user is writing an
executable, or using fbl
only in the private portions of their library. If
fbl
is being used anywhere in the public headers of a user's library,
public_deps
should be used instead.