I have a pathfinding algorithm library, and a test module that graphically displays every step the algorithm takes. The idea is that one should be able to conditionally compile either a test for the algorithm, or the library. So the algorithm object should remain lean and not store nor return any data that wouldn't be needed outside of testing.
At the moment I'm using a text file as intermediate storage, writing the steps into it whenever they happen. The test module read them from there when the search is done.
The test module is a child of the algorithm module.
Is there a better intermediate storage? Some way to get a static mutable vector perhaps? I also read something about task local storage but it's not documented well.
Edit:
Unfortunately, something like this doesn't seem to work:
pub struct JumpPointSearch {
closed_set: Vec<Node>,
open_set: PriorityQueue<Node>,
#[cfg(demo)] steps: Vec<(Point2<uint>,Point2<uint>)>
}
impl JumpPointSearch {
pub fn new() -> JumpPointSearch {
if cfg!(demo) {
JumpPointSearch {
closed_set: Vec::with_capacity(40),
open_set: PriorityQueue::with_capacity(10),
steps: Vec::new()
}
} else {
JumpPointSearch { // error: missing field: `steps`
closed_set: Vec::with_capacity(40),
open_set: PriorityQueue::with_capacity(10),
}
}
}
}
This also doesn't work:
pub struct JumpPointSearch {
closed_set: Vec<Node>,
open_set: PriorityQueue<Node>,
#[cfg(demo)] steps: Vec<(Point2<uint>,Point2<uint>)>
}
impl JumpPointSearch {
pub fn new() -> JumpPointSearch {
JumpPointSearch {
closed_set: Vec::with_capacity(40),
open_set: PriorityQueue::with_capacity(10),
#[cfg(demo)] steps: Vec::new()
// error: expected ident, found `#`
}
}
}
For conditional definitions like this, you need to use the attribute form, not the macro. That is
#[cfg(demo)]
pub fn new() -> JumpPointSearch {
JumpPointSearch {
closed_set: Vec::with_capacity(40),
open_set: PriorityQueue::with_capacity(10),
steps: Vec::new()
}
}
#[cfg(not(demo))]
pub fn new() -> JumpPointSearch {
JumpPointSearch {
closed_set: Vec::with_capacity(40),
open_set: PriorityQueue::with_capacity(10),
}
}
The cfg!
macro just expands to true
or false
depending if the configuration matches, that is, neither branch is eliminated, and type checking (etc.) still happens for both, which is why it doesn't work: one of the struct initialisations doesn't match the definition.
However, I think there's higher-level approachs to this, they're both pretty similar, but the gist is to have the JumpPointSearch
implementation the same always, and to just have the steps
field change.
struct ReleaseSteps;
struct DebugSteps {
steps: Vec<(Point2<uint>, Point2<uint>)>
}
trait Step {
fn register_step(&mut self, step: (Point<uint>, Point<uint>));
}
impl Step for ReleaseSteps {
#[inline(always)] // ensure that this is always 0 overhead
fn register_step(&mut self, step: (Point<uint>, Point<uint>)) {}
}
impl Step for DebugSteps {
fn register_step(&mut self, step: (Point<uint>, Point<uint>)) {
self.steps.push(step)
}
}
struct JumpPointSearch<St> {
closed_set: Vec<Node>,
open_set: PriorityQueue<Node>,
steps: St
}
impl<St: Step> JumpPointSearch<St> {
fn new(step: St) -> JumpPointSearch<St> { ... }
}
Then, whenever you would've updated steps
before, just call register_step
: if it's in "release mode", that will be ignored, and if it's in "debug mode", it will be registered.
This has the unfortunate downside of requiring generics etc. (could be ameliorated with careful use of;
- cfg
e.g. #[cfg(demo)] impl JumpPointSearch<DebugSteps> { fn new() -> JumpPointSearch<DebugSteps> { ... } }
+ the corresponding #[cfg(not(demo))]
with ReleaseSteps
.
- default type parameters, e.g. struct JumpPointSearch<St = ReleaseSteps> { ... }
(these are still experimental)
This approach allows you to easily extend the diagnostics by providing more hooks in the Steps
trait (e.g. you could provide an option to record even more expensive-to-compute information, and have three levels of Step
: release, debug and super-debug).
cfg
"generics"The other option is to use the types defined in the previous section with cfg
to select DebugSteps
or ReleaseSteps
in a fixed way, that is:
#[cfg(demo)]
use Steps = DebugSteps;
#[cfg(not(demo))]
use Steps = ReleaseSteps;
struct JumpPointSearch {
closed_set: Vec<Node>,
open_set: PriorityQueue<Node>,
steps: Steps
}
If you don't need the extensibility and are happy just making a fixed selection at compile time, this is my recommended approach: you don't need many #[cfg]
s at all (just the two above, and possibly 2 more, depending on how you handle extracting data from the steps
field) and you don't have to throw a lot of generics around.
Two small points:
use
like that allows Steps::new()
to work, and means you can write Steps
whereever you need the type. (The two other possibilities have issues: using the struct fields, i.e. #[cfg(demo)] steps: DebugSteps
, fails both; and using #[cfg(demo)] type Steps = DebugSteps;
doesn't (yet) allow Steps::new()
.)Steps
trait for this one, and just impl
the method directly, that is: impl ReleaseSteps { fn new() -> ReleaseSteps { ... } fn register_steps(...) {} }
and similarly for DebugSteps
.