blob: 917a54899d4ac51d381c34f35cd2b9574663f2e9 [file] [log] [blame] [view]
# Advanced scenarios
By default, most of the behavior of intrusive containers in `fbl::` are designed
to provide as much safety as possible at compile time, typically by disallowing
use patterns that might easily lead to mistakes at compile time.
That said, there are certain advanced scenarios where a user may choose to
bypass these compile time safeties in order to deliberately permit certain
behaviors. Whenever you choose to use one of these options, make sure that you
have thought carefully about the implications of taking the safety off, and be
sure that you are using the functionality in a safe way.
This section of the guide will show you how to:
1. [Use `fbl::NodeOptions` to opt into advanced behaviors](#node-options)
2. [Control the copy/move-ability of objects while they are in a container.](#copy-move-behavior)
3. [Permit an object tracked by `unique_ptr` to be contained in multiple container types](#multiple-unique)
4. [Clear a container of raw pointers in O(1) time](#clear-unsafe)
5. [Remove an object from a container without a reference to the container](#direct-remove)
## Controlling advanced options with `fbl::NodeOptions` {#node-options}
In order to control some advanced options at compile time, node state objects
(as well as their associated mix-ins) can take a bit-flag style constant, which
can be used to change specific behaviors. Options may be combined using the `|`
operator. By default, options are the second template parameter of either a
`NodeState` or `Listable`/`Containable` type, and the third template parameter
of a `TaggedListable`/`TaggedContainable` mix-in. These options always default
to `fbl::NodeOptions::None`. The syntax looks like this:
```cpp
class SimpleObject :
public fbl::DoublyLinkedListable<SimpleObject*, fbl::NodeOption::OptionFoo> { /* ... */ };
class MoreComplexObject :
public fbl::ContainableBaseClasses<
fbl::TaggedSinglyLinkedListable<MoreComplex*, Tag1, fbl::NodeOption::OptionBar>,
fbl::TaggedWAVLTreeContainable <MoreComplex*, Tag2,
fbl::NodeOption::OptionA | fbl::NodeOption::OptionB>> {
// ...
};
class ExplicitNodesObject {
public:
// ...
private:
// ...
static constexpr fbl::NodeOptions kOptions = fbl::NodeOption::OptionX |
fbl::NodeOption::OptionY |
fbl::NodeOption::OptionZ;
fbl::WAVLTreeNodeState<ExplicitNodesObject*, kOptions> wavl_node_state_;
};
```
## Controlling the copy/move behavior of objects while they are in a container {#copy-move-behavior}
Copying or moving node state while its object is in a container is not a legal
operation can cannot be permitted. Consider the following:
```cpp
fbl::DoublyLinkedList<Obj*> the_list;
ASSERT(!the_list.is_empty());
Obj the_obj;
the_list.insert_after(the_list.begin());
Obj another_obj{the_obj};
Obj yet_another_object;
the_obj = yet_another_object;
```
`the_obj` exists in the list after the first node. If you were to allow the node
state to be copied into `another_obj` via the default copy constructor, you would
have two objects with two copies of the bookkeeping. `another_obj` would
incorrectly think that it was in the container, and will now attempt to assert
when destroyed.
Worse, if you attempt to remove the object by calling
`the_list.erase(another_object)`, you are attempting to erase an object from a
container in an incoherent state. In this case, the prev and next pointers of
another object point to the first object in the list, the object that used to
be after `begin()` at the start of the example, but the next pointer of
`*begin()` is pointing to `the_obj`, and likewise for the prev pointer of the
next object in the sequence. While the specific behavior will vary on the type
of container and the specific implementation of erase, what is going to happen
is undefined behavior and cannot end well.
Finally, when the example code assigns the newly stack constructed
`yet_another_object` to `the_obj`, if the node state data were to be copied from
`yet_another_object`, `the_obj` would suddenly think it is not in the list even
though the objects on either side of it held pointers to it.
Any way you look at it, permitting the node state data to be copied will corrupt
the container's structure and cannot be allowed to happen, and the same goes for
the definition of most move operations.
To prevent mistakes like this, the default behavior of `fbl::` node state
objects is to disallow copy construction/assignment as well as move
construction/assignment. Any attempt to invoke the copy/move
constructor/assignment operator will result in a `static_assert` and a failure
to compile.
What about when objects are not in a container? Shouldn't copy/move be
permitted then? The short answer is sure, but in the interest of safety, it is
considered to be allowed only if the code author has opted into the behavior.
In order to opt in, you may use the following `NodeOptions` with their mix-in
or node storage types.
* `fbl::NodeOptions::AllowCopy`
* `fbl::NodeOptions::AllowMove`
* `fbl::NodeOptions::AllowCopyAndMove`
Setting `AllowCopy` will permit copy (l-value) construction and assignment,
while setting `AllowMove` will permit move (r-value) construction and
assignment. `AllowCopyAndMove` is simply shorthand for the two of them
combined.
During the operation itself, the node state object will `ZX_DEBUG_ASSERT` that
neither the source object is not in a container for construction and that
neither the source nor the destination object exist in containers during
assignment. Regardless of whether or not `ZX_DEBUG_ASERT`s are enabled, the
source and destination objects' states will *never* be modified.
For example:
```cpp
struct Point : fbl::DoublyLinkedListable<std::unique_ptr<Point>,
fbl::NodeOptions::AllowCopy> {
float x, y;
};
fbl::DoublyLinkedList<std::unique_ptr<Point>> all_points;
void AddCopy(const Point& pt) {
// If pt is in a list, this will assert. If asserts are off, pt will remain
// where it is and new_pt will not start life in a container.
auto new_pt = std::make_unique<Point>(pt);
all_points.push_back(std::move(new_pt));
}
```
So, what if you want to permit copying or moving of an object even while it is
_in_ a container? For example, what if you wanted to clone your list of points
making use of your copy constructor in order to do so? Users may opt into this
behavior as well by passing appropriate combinations of the following:
* `fbl::NodeOptions::AllowCopyFromContainer`
* `fbl::NodeOptions::AllowMoveFromContainer`
* `fbl::NodeOptions::AllowCopyAndMoveFromContainer`
The behavior described above remains the same; node states will never be
changed. New objects will start outside of any container and source objects will
remain wherever they are. The only difference between the `FromContainer`
version of this option vs the non-`FromContainer` version is that the
`FromContainer` version will never assert. So, you could clone your list of
points with the following.
```cpp
struct Point : fbl::DoublyLinkedListable<std::unique_ptr<Point>,
fbl::NodeOptions::AllowCopyFromContainer> {
float x, y;
};
using PointList = fbl::DoublyLinkedList<std::unique_ptr<Point>>;
PointList CloneList(const PointList& list) {
PointList ret;
for (const auto& point : list) {
ret.push_back(std::make_unique<Point>(point));
}
return ret;
}
```
## Allowing objects tracked by `unique_ptr` to exist in multiple containers {#multiple-unique}
Usually, it would be a mistake to define an object that can exist in
multiple containers concurrently, while tracking those objects in their
containers using `unique_ptr` semantics. In theory, it should be impossible
for two different containers to track the same object at the same time,
each using something like a `unique_ptr` as this would violate the uniqueness of
the pointer.
To assist in preventing any mistakes here, `ContainableBaseClasses` will not
permit the use of a `std::unique_ptr` pointer type for any of the specified
mix-ins, unless the length of the list of containable base classes is exactly 1.
```cpp
struct Tag1 {};
struct Tag2 {};
// This is legal
class Obj : public fbl::ContainableBaseClasses<
fbl::TaggedSinglyLinkedListable<std::unique_ptr<Obj>, Tag1>> { /* ... */ };
// This is not
class Obj : public fbl::ContainableBaseClasses<
fbl::TaggedSinglyLinkedListable<std::unique_ptr<Obj>, Tag1>,
fbl::TaggedSinglyLinkedListable<std::unique_ptr<Obj>, Tag2>> { /* ... */ };
// Neither is this
class Obj : public fbl::ContainableBaseClasses<
fbl::TaggedSinglyLinkedListable<std::unique_ptr<Obj>, Tag1>,
fbl::TaggedSinglyLinkedListable<Obj*, Tag2>> { /* ... */ };
```
There are, however, legitimate uses for types that can exist in multiple
containers, which are managed using `std::unique_ptr`s.
First, you may have a situation where an object can exist in two different types
of data structure (perhaps a list and a tree), but never the same data structure
at the same time. If the uses of the structure are completely disjoint, you may
wish to relax the default restriction.
A second reason you might want to allow this is because you have an object whose
life is tracked by a container using `std::unique_ptr`, but for which you want
to allow objects to exist in containers on a temporary basis in order to more
easily implement some sort of algorithm. Perhaps a set of objects needs to be
filtered into a temporary list and then passed to a function that will operate
on the filtered set. Or, perhaps they need to be placed into a temporary
WAVLTree with a custom sorting/keys in order to check for duplicates.
Whatever the reason, you can permit this behavior by passing the
`AllowMultiContainerUptr` option to the node state types that you use. Here is
an example for the disjoint container use case:
```cpp
struct FreeObjTag {};
struct ActiveObjTag {};
class Obj : public fbl::ContainableBaseClasses<
fbl::TaggedSinglyLinkedListable<std::unique_ptr<Obj>, FreeObjTag,
fbl::NodeOptions::AllowMultiContainerUptr>,
fbl::TaggedWAVLTreeContainable<std::unique_ptr<Obj>, ActiveObjTag,
fbl::NodeOptions::AllowMultiContainerUptr>> {
public:
using FreeStack = fbl::TaggedSinglyLinkedList<std::unique_ptr<Obj>, FreeObjTag>;
using ActiveSet = fbl::TaggedWAVLTree<UniqueId, std::unique_ptr<Obj>, ActiveObjTag>;
// ...
UniqueId GetKey() const { return unique_id_; }
void AssignId(UniqueId id) {
ZX_DEBUG_ASSERT(!fbl::InContainer<ActiveObjTag>(*this));
unique_id_ = id;
}
// ...
private:
// ...
};
fbl::Mutex obj_lock_;
Obj::FreeStack free_objects_ TA_GUARDED(obj_lock_);
Obj::ActiveSet active_objects_ TA_GUARDED(obj_lock_);
zx_status_t ActivateObject(UniqueId id) {
fbl::AutoLock lock(&obj_lock_);
if (free_objects_.is_empty()) {
return ZX_ERR_NO_MEMORY;
}
auto ptr = free_objects_.pop_front();
ptr.AssignId(id);
active_objects_.insert(std::move(ptr));
return ZX_OK;
}
```
## Allowing O(1) clearing of containers with `clear_unsafe()` {#clear-unsafe}
As noted in the [Lifecycle checks](#lifecycle-checks) section, containers of
unmanaged pointers may not destruct with objects still in them, and objects
cannot destruct while they think that they are still in a container. Either
behavior is considered to be an error and will trigger an assert in a debug
build.
What if you had a situation where you didn't care that your objects still
thought that they were in a container when they destructed? Perhaps you
allocated a contiguous slab of memory and carved it up into object that you
then placed onto a free list. If you want to free your slab of memory, and you
know that all of the objects have been returned to the free list, then why
bother walking the list to zero out all of the linked list bookkeeping? This
would just be wasted work.
You can bypass these checks and skip the mandatory O(N) unlinking of the list by
using the `AllowClearUnsafe` NodeOption on your objects. When used, the asserts
present in the node state object are skipped, and a method on the container
called `clear_unsafe()` becomes available for use. `clear_unsafe()` will simply
reset the container to its original empty state making no effort to clean up
the objects' node states. This is a simple O(1) operation. Attempting to call
`clear_unsafe()` on a container that uses a node state object without this flag
will trigger a `static_assert`. Here is a simple example of what this would
look like:
```cpp
class SlabObject :
public fbl::SinglyLinkedListable<SlabObject*,
fbl::NodeOptions::AllowClearUnsafe> { /* ... */ };
static fbl::Mutex slab_lock;
static SlabObject* slab_memory TA_GUARDED(slab_lock) = nullptr;
static fbl::SizedSinglyLinkedList<SlabObject*> TA_GUARDED(slab_lock) free_objects;
static constexpr size_t kSlabSize = (1 << 20); // One MB of objects
static constexpr size_t kSlabCount = kSlabSize / sizeof(SlabObject);
zx_status_t InitSlab() {
fbl::AutoLock lock(&slab_lock);
if ((slab_memory != nullptr) || !free_objects.is_empty()) {
return ZX_ERR_BAD_STATE;
}
fbl::AllocChecker ac;
slab_memory = new (&ac) SlabObject[kSlabCount];
if (!ac.check()) {
return ZX_ERR_NO_MEMORY;
}
for (size_t i = 0; i < kSlabCount; ++i) {
free_objects.push_front(slab_memory + i);
}
return ZX_OK;
}
SlabObject* GetFreeObj() {
fbl::AutoLock lock(&slab_lock);
return !free_objects.is_empty() ? free_objects.pop_front() : nullptr;
}
void ReturnObj(SlabObject* obj) {
fbl::AutoLock lock(&slab_lock);
ZX_DEBUG_ASSERT(obj != nullptr);
free_objects.push_front(obj);
}
zx_status_t DeinitSlab() {
fbl::AutoLock lock(&slab_lock);
// If not all of our objects have returned, or if we don't have any slab
// memory allocated, then we cannot de-init our slab.
if ((slab_memory == nullptr) || (free_objects.size() != kSlabCount)) {
return ZX_ERR_BAD_STATE;
}
// Great, reset the free list with clear unsafe. This basically just sets the
// head pointer to nullptr.
free_objects.clear_unsafe();
// Now give our memory back. Since our objects are flagged with
// AllowClearUnsafe, node state destructors do nothing. Provided that
// SlabObject destructors do nothing, this delete should just return memory to
// the heap and not need to call N destructors.
delete[] free_objects;
free_objects = nullptr;
return ZX_OK;
}
```
## Directly removing objects from whatever container instance they are in. {#direct-remove}
In general, even though it is sometimes possible, it is not considered best
practice to design code that needs to remove objects directly from a container
without having a reference to the container itself. As a design principle, users
of intrusive containers should always be aware of which container types and
instances objects exist in at all times. Still, sometimes direct removal might
be the easiest and best option available.
Containers that track size by default might require an O(n) traversal of the
data structure in order to find the container to update the bookkeeping if nodes
are removed from the container without knowledge of the container instance.
Therefore, these containers do not support direct removal. Other container
types, such as a `SinglyLinkedList`, simply cannot do this as they lack a
back-pointer to their previous node.
It is, however, possible for an unsized doubly linked list to support direct
node removal. To enable this, add the `AllowRemoveFromContainer` to the node
state's `NodeOption`s. When enabled, node state structures will have a
`RemoveFromContainer()` method available. Calling RemoveFromContainer is
identical to calling InContainer. It may be called directly from the object if
there is no ambiguity, using explicit types to select the container to remove
from when inheritance produces ambiguity, or using the top level
`fbl::RemoveFromContaier<Tag>(obj_ref)` call when using the
`ContainableBaseClasses` helper. See [`InContainer()`](membership_tests.md#single-container)
Consider the following use case. You have a bunch of jobs that need to be
processed by several stages of a pipeline. The pipeline stages each have a
queue of pending work that threads take jobs from, process, and then queue
to the next stage of a pipeline.
If you want to cancel a job while it is in flight, how do you easily know which
pipeline stage it is in? One answer might be that you don't need to, as long as
you can directly remove it from the processing stage it is currently in. This
might end up looking like the following:
```cpp
struct PipelineTag {};
struct ActiveTag {};
fbl::Mutex pipeline_lock;
class Job : public fbl::RefCounted<Job>,
public fbl::ContainableBaseClasses<
fbl::TaggedDoublyLinkedListable<fbl::RefPtr<Job>, PipelineTag,
fbl::NodeOptions::AllowRemoveFromContainer>,
fbl::TaggedWAVLTreeContainable<fbl::RefPtr<Job>, ActiveTag>> {
public:
// ...
UniqueId GetKey() const { return unique_id_; }
bool is_canceled() const TA_REQ(pipeline_lock) { return cancel_flag_; }
void set_canceled() TA_REQ(pipeline_lock) { cancel_flag_ = true; }
// ...
private:
bool cancel_flag_ TA_GUARDED(pipeline_lock) = false;
};
using PipelineQueue = fbl::TaggedDoublyLinkedList<fbl::RefPtr<Job>, PipelineTag>;
std::array<PipelineQueue, 10> pipeline_stages TA_GUARDED(pipeline_lock);
fbl::TaggedWAVLTree<fbl::RefPtr<Job>, ActiveTag> active_jobs TA_GUARDED(pipeline_lock);
zx_status_t QueueJob(fbl::RefPtr<Job> job) {
ZX_DEBUG_ASSERT(job != nullptr);
{
fbl::AutoLock lock(&pipeline_lock);
// Can't queue a job for processing if it is already being processed.
if (fbl::InContainer<ActiveTag>(*job)) {
return ZX_ERR_BAD_STATE;
}
// If we are not in the active set, then we had better not be in any of the
// pipeline stages.
ZX_DEBUG_ASSERT(!fbl::InContainer<PipelineTag>(*job));
// Put the job into the active set and into the first pipeline stage.
active_jobs.insert(job);
pipeline_stages[0].push_back(std::move(job));
}
SignalPipelineStage(0);
}
void WorkThread(size_t pipeline_stage) {
ZX_DEBUG_ASSERT(pipeline_stage < pipeline_stages.size());
PipelineQueue& our_stage = pipeline_stages[pipeline_stage];
PipelineQueue* next_stage = ((pipeline_stage + 1) < pipeline_stages.size())
? (pipeline_stages + pipeline_stage + 1)
: nullptr;
while (!QuitTime()) {
fbl::RefPtr<Job> job;
{
// If there is work in our stage, take it out and get to work.
fbl::AutoLock lock(&pipeline_lock);
if (!our_stage.is_empty()) {
job = our_stage.pop_front();
}
}
// Did we not find a job? Just wait for something to do then.
if (job == nullptr) {
WaitForPipelineStageWorkOrQuit(pipeline_stage);
continue;
}
// Do the job.
ProcessJob(job, pipeline_stage);
// If the job was canceled or reached the end of the pipeline, we will call
// a handler to take care of it once we are out of the lock.
void(*handler)(fbl::RefPtr<Job>) = nullptr;
{
fbl::AutoLock lock(&pipeline_lock);
if (job->is_canceled()) {
// Handle job cancellation if it was flagged for cancel while we were
// working. No need to take it out of the active set, the cancel
// operation should have already done that for us.
ZX_DEBUG_ASSERT(!fbl::InContainer<ActiveTag>(*job));
handler = HandleCanceledJob;
} else if (next_stage != nullptr) {
// Queue to the next stage if there is one.
next_stage->push_back(std::move(job));
signal_next_stage = true;
} else {
// End of pipeline. This job is finished, remember to take it out of
// the active set.
ZX_DEBUG_ASSERT(fbl::InContainer<ActiveTag>(*job));
active_jobs.erase(*job);
handler = HandleFinishedJob;
}
}
// Now that we are out of the lock, either signal the next stage so that it
// knows that it might have some work, or call the chosen handler on the job.
if (handler) {
ZX_DEBUG_ASERT(job != nullptr);
handler(std::move(job));
} else {
SignalPipelineStage(pipeline_stage + 1);
}
}
}
zx_status_t CancelJob(UniqueId id) {
fbl::RefPtr<Job> canceled_job;
{
fbl::AutoLock lock(&pipeline_lock);
// Is there an active job with the provided ID?
auto iter = active_jobs.find(id);
if (!iter.IsValid()) {
return ZX_ERR_NOT_FOUND;
}
// No matter what, the job is no longer active. Take its reference back from
// the active job set.
fbl::RefPtr<Job> job = active_jobs.erase(iter);
// Flag the job as canceled.
job->set_canceled();
// If the job is in a pipeline stage, then no thread is currently working on
// it. We can just pull it out of whatever stage we are in and we are done.
if (fbl::InContainer<PipelineTag>(*job)) {
canceled_job = fbl::RemoveFromContainer<PipelineTag>(*job);
}
}
// Now that we are out of the lock, if we were the ones to pull the job out of
// the pipeline, we should hand it over to the cancel handler.
HandleCanceledJob(std::move(canceled_job));
return ZX_OK;
}
```