Abstract over how a T is stored and accessed by using generic "containers", bounded by
the container traits here. A container owns a T and can provide references to it, or be
consumed to return the inner T (if T is Sized).
Some containers are fallible, or can only provide immutable references and not mutable references. The container traits are meant to be specific enough that a generic container can be bound more tightly by the interface it needs, to support more container implementations.
Skip to here for a list of the provided container implementations.
Motivation
Someone who only needs to use your type from a single thread, will never clone it, and will only
use a single concrete type for a generic or dyn will likely have very different preferences
from someone who wants to use your type from multiple threads and instantiate a bulky generic
type with many combinations of generic parameters.
This concern is likely not even applicable for Clone + Send + Sync types that have no
generics, and in the other direction, the two use cases may simply be far too different to
cleanly unify.
But when "how will this T be stored?" is a major blocker against flexibly supporting both
sorts of uses in a performant way, this crate can help. The traits here allow a type
to abstract over how something is held and accessed by using a generic "container", and
blanket implementations for Trait can implement Trait for any container wrapping
dyn Trait, or effectively T: Trait in general with the help of a container wrapper type
[^blanket-container-t].
Choices like these can be deferred to the consumer of your types (or traits):
- Is the tradeoff between atomic refcounts and
SendorSyncworth it? - Will the consumer never clone your type, or is it necessary to be able to cheaply clone
something with a copyable or refcounted container (which could be signaled by the container
implementing
Copyordupe::Dupe, or perhaps by the container implementingCloneeven when the contained type is notClone)? - Will the impact of monomorphized generics on the binary size outweigh their better
optimization compared to
dyn, or are some of the concrete types involved not known until runtime? If so, a user may prefer or need adyntrait object, and from the first two bullets, maybe a user would needRc<dyn Trait>rather thanBox<dyn Trait>, and in general may want something likeContainer<dyn Trait>[^container-dyn-trait].
Particular traits worth abstracting over are Clone (or a cheap clone like dupe::Dupe
in particular), Send, and Sync.
Additionally, trait authors may wish to allow any generics bound by your traits to optionally
use dyn instead of monomorphizing everything; a standard way to allow for this is to
implement YourTrait for Box<dyn YourTrait> and perhaps a few other smart pointers, but even
better is providing a blanket implementation for YourTrait for any
C: ?Sized + Container<dyn YourTrait> [^container-dyn-trait].
Example
use FragileContainer;
// Whenever something is generic over `MyTrait`, thanks to this blanket impl, we could opt into
// using `Box<dyn MyTrait>`, `Arc<dyn MyTrait>`, and so on. This way, being generic over
// `MyTrait` gives the user a strict superset of the abilities they'd have if we only used
// `dyn MyTrait` internally.
;
// This is effectively the same thing you'd get if you used a `dyn` object instead of a generic
// above; the user of your struct loses nothing.
type NeedsToCalculateSomethingDyn = ;
Container Traits
Eight main container traits are provided here, with every combination of the following aspects:
- Mutability: whether a container can provide mutable references to the value stored in the container, in addition to immutable references.
- Fallibility: a container is fallible if
try_get_refortry_get_mutcan fail, andget_reforget_mutcannot be provided. - Fragility: a fragile container is not guaranteed to support reentrancy, and may panic or
deadlock if
try_get_ref(or similar) is called by a thread that already has a live reference to the value stored in the container.
Mutability is indicated by the prefix Mut, fallibility by the prefix Try, and fragility
by the prefix Fragile. They are combined in the order "FragileTryMutContainer".
There are also two de facto trait aliases which replace "FragileTry" with "Base" in the two
FragileTry*Container traits.
Except for the Base*Container traits intended as aliases, implementors of the traits
must manually implement each of them; there are no blanket [Container] implementations for
TryContainer types whose error types are uninhabited, for instance.
When bounding a generic by a container trait, you should generally bound by the minimum
container interface you need, plus any other marker trait needed, like Clone (or a cheap
clone like dupe::Dupe), Send, and Sync. Conversely, you should bound the T which
the container is required to store as tightly as you can, especially by Sized, Send,
and Sync.
Fragility: Potential Panics or Deadlocks
The Ref associated types of some container implementations (and RefMut for mutable
containers) may have nontrivial Drop impls that interfere with other attempts to borrow from
the container, and possibly from other clones of the container referencing the same inner data.
Such a container might be "fragile", unless it implements additional container traits
indicating that it is not.
Being "fragile" means that a single thread should not attempt to get multiple live references
to the T in the container, whether from the same container instance or clones referencing the
same inner T. Doing so risks a panic or deadlock (such as in the case of Arc<Mutex<T>>).
In other words, the fragile container traits do not guarantee that the container handles
reentrancy gracefully. A thread should drop any borrow obtained from the fragile container's
methods accessing its inner T before a new reference to the inner T can be obtained without
any risk of panic or deadlock.
Containers
Common examples of containers include T itself, Box<T>, Rc<T>, Rc<RefCell<T>>, Arc<T>,
Arc<RwLock<T>>, and Arc<Mutex<T>>.
Additionally, container traits are implemented for [CheckedRcRefCell<T>] (from this crate) and
Arc<ThreadCheckedMutex<T>> (when the thread-checked-lock feature is enabled).
Note that CheckedRcRefCell<T> and Arc<ThreadCheckedMutex<T>> essentially shift how the
runtime invariants of a RefCell or Mutex are enforced; with a fragile implementation, the
user is required to enforce them (on pain of panics or deadlocks), while with a fallible
implementation, such usage will still fail, but not fatally. Defaulting to the fragile
implementations is likely the best choice.
Some container implementations choose to panic if a poison error is encountered, as a poison error can only occur if another thread has already panicked.
Other crates may implement container traits for their own types.
Provided Container Implementations
This crate provides the following container implementations:
-
For
MutContainer<T>(and its supertraits):TitselfBox<T>
-
For
Container<T>(and its supertraits):Rc<T>Arc<T>
-
For
FragileMutContainer<T>(and its supertraits):Rc<RefCell<T>>Arc<RwLock<T>>(implementation may panic on poison)Arc<Mutex<T>>(implementation may panic on poison)
-
For
TryMutContainer<T>(and its supertraits):CheckedRcRefCell<T>Arc<ThreadCheckedMutex<T>>(only if thethread-checked-lockfeature is enabled)
Container Kind Traits
Currently, Rust doesn't allow bounds like
where C: for<T: Send> Container<T> + Clone + Send + Sync.
The solution is to define an extra trait with whatever you need as the bounds of a GAT
(generic associated type):
use FragileMutContainer;
use Dupe;
If some data needs thread-safe mutability, but you don't want to pay the cost of a lock for read-only data, you can use multiple GATs:
use ;
use Dupe;
Such a trait is called a "container kind trait" (with implementations being "container kinds", just
as implementations of the container traits are "containers"). Relevant characteristics for a
container kind's container include the eight container traits, Send + Sync bounds (possibly only
when T is Send + Sync, or just Send), Dupe, whether the GAT allows T to be unsized, and
Debug bounds.
Not every conceivably-useful container kind trait is provided, as there would be exponentially-many such traits. Note also that creating simple container kind traits that can be combined into more complicated bounds does work, but not well. A container kind should set the GAT of each container kind trait it implements to the same container type; this can be asserted or required with Rust's type system, but the trait solver doesn't understand it very well. Such container kind traits would likely not be pleasant to use.
When the kinds feature is enabled, container kinds and container kind traits for common sorts of
containers are provided. They do not use Dupe bounds, and do not mess with type-equality
shenanigans that confuse the trait solver.
Features
std: enables support forArc<Mutex<T>>andArc<RwLock<T>>. Enabled by default. Implies theallocfeature.alloc: enables container implementations based onBox,Rc,Arc, andRefCell, includingCheckedRcRefCell. Withoutalloc, the container traits andGenericContainerare still available, andTis a container for itself. Enabled by default.kinds: provides several container kinds and container kind traits (see above).thread-checked-lock: if enabled,TryMutContainer<T>is implemented for Arc<ThreadCheckedMutex<T>>. Implies thestdfeature.serde: derivesSerializeandDeserializeforGenericContainerand, ifallocis enabled,CheckedRcRefCell.
MSRV
Rust 1.85, the earliest version of the 2024 edition, is supported.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.
[^blanket-container-t]: To satisfy the trait solver and avoid conflicting trait implementations,
a GenericContainer<T, C> struct is provided. It is not necessary for blanket
implementations of YourTrait for any container holding dyn YourTrait, but should be used for
blanket implementations for GenericContainer<T, C> for any container C holding a
T: ?Sized + YourTrait, or similar.
[^container-dyn-trait]: Container<dyn Trait> might not be the best choice;
[FragileContainer] is preferred if possible. Just as functions are encouraged to take
FnOnce or FnMut callbacks rather than Fn (if possible), it would be best to accept
a fragile container, if the Trait's methods don't expose some potential for reentrancy with
the container holding the dyn Trait. For example, if any of a ReentrantTrait's methods
take a &self parameter and take inputs that could, potentially, have some way of getting a
reference to the wrapping [FragileContainer], then implementing such a method of
ReentrantTrait for FragileContainer<dyn ReentrantTrait> would likely begin by calling
get_ref on the container. Then, the other inputs of the method could call get_ref on
their reference to the container. Containers which are actually fragile (and don't implement
TryContainer) are probably refcounted and cloneable, so changing &self to &mut self
doesn't help: if ReentrantTrait provides a user an opportunity to run arbitrary
code inside one of its &self or &mut self methods, there's a potential problem.
As such, while FragileContainer<dyn ReentrantTrait> could be used if you are careful,
it would not work in every situation that the ReentrantTrait interface would normally
require; therefore, a blanket implementation of ReentrantTrait for anything implementing
Container<dyn ReentrantTrait> could be provided, but doing so for
FragileContainer<dyn ReentrantTrait> would make it easy to hand a fragile container to
something expecting a normal ReentrantTrait, leading to potential panics or deadlocks.