almost_enough/boxed.rs
1//! Boxed dynamic dispatch for Stop.
2//!
3//! This module provides [`BoxedStop`], a heap-allocated wrapper that enables
4//! dynamic dispatch without monomorphization bloat.
5//!
6//! # When to Use
7//!
8//! Generic functions like `fn process(stop: impl Stop)` are monomorphized
9//! for each concrete type, increasing binary size. `BoxedStop` provides a
10//! single concrete type for dynamic dispatch:
11//!
12//! ```rust
13//! use almost_enough::{BoxedStop, Stop};
14//!
15//! // Monomorphized for each Stop type - increases binary size
16//! fn process_generic(stop: impl Stop) {
17//! // ...
18//! }
19//!
20//! // Single implementation - no monomorphization bloat
21//! fn process_boxed(stop: BoxedStop) {
22//! // ...
23//! }
24//! ```
25//!
26//! # Alternatives
27//!
28//! For borrowed dynamic dispatch with zero allocation, use `&dyn Stop`:
29//!
30//! ```rust
31//! use almost_enough::{StopSource, Stop};
32//!
33//! fn process(stop: &dyn Stop) {
34//! if stop.should_stop() {
35//! return;
36//! }
37//! // ...
38//! }
39//!
40//! let source = StopSource::new();
41//! process(&source);
42//! ```
43
44use alloc::boxed::Box;
45
46use crate::{Stop, StopReason};
47
48/// A heap-allocated [`Stop`] implementation.
49///
50/// This type provides dynamic dispatch for `Stop`, avoiding monomorphization
51/// bloat when you don't need the performance of generics.
52///
53/// # Example
54///
55/// ```rust
56/// use almost_enough::{BoxedStop, StopSource, Stopper, Unstoppable, Stop};
57///
58/// fn process(stop: BoxedStop) {
59/// for i in 0..1000 {
60/// if i % 100 == 0 && stop.should_stop() {
61/// return;
62/// }
63/// // process...
64/// }
65/// }
66///
67/// // Works with any Stop implementation
68/// process(BoxedStop::new(Unstoppable));
69/// process(BoxedStop::new(StopSource::new()));
70/// process(BoxedStop::new(Stopper::new()));
71/// ```
72pub struct BoxedStop(Box<dyn Stop + Send + Sync>);
73
74impl BoxedStop {
75 /// Create a new boxed stop from any [`Stop`] implementation.
76 #[inline]
77 pub fn new<T: Stop + 'static>(stop: T) -> Self {
78 Self(Box::new(stop))
79 }
80}
81
82impl Stop for BoxedStop {
83 #[inline]
84 fn check(&self) -> Result<(), StopReason> {
85 self.0.check()
86 }
87
88 #[inline]
89 fn should_stop(&self) -> bool {
90 self.0.should_stop()
91 }
92}
93
94impl core::fmt::Debug for BoxedStop {
95 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
96 f.debug_tuple("BoxedStop").finish()
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::{StopSource, Stopper, Unstoppable};
104
105 #[test]
106 fn boxed_stop_from_unstoppable() {
107 let stop = BoxedStop::new(Unstoppable);
108 assert!(!stop.should_stop());
109 assert!(stop.check().is_ok());
110 }
111
112 #[test]
113 fn boxed_stop_from_stopper() {
114 let stopper = Stopper::new();
115 let stop = BoxedStop::new(stopper.clone());
116
117 assert!(!stop.should_stop());
118
119 stopper.cancel();
120
121 assert!(stop.should_stop());
122 assert_eq!(stop.check(), Err(StopReason::Cancelled));
123 }
124
125 #[test]
126 fn boxed_stop_is_send_sync() {
127 fn assert_send_sync<T: Send + Sync>() {}
128 assert_send_sync::<BoxedStop>();
129 }
130
131 #[test]
132 fn boxed_stop_debug() {
133 let stop = BoxedStop::new(Unstoppable);
134 let debug = alloc::format!("{:?}", stop);
135 assert!(debug.contains("BoxedStop"));
136 }
137
138 #[test]
139 fn boxed_stop_avoids_monomorphization() {
140 // This function has a single concrete implementation
141 fn process(stop: BoxedStop) -> bool {
142 stop.should_stop()
143 }
144
145 // All these use the same process function
146 assert!(!process(BoxedStop::new(Unstoppable)));
147 assert!(!process(BoxedStop::new(StopSource::new())));
148 assert!(!process(BoxedStop::new(Stopper::new())));
149 }
150}