2

I want to provide default values for structs to be used only within tests (and not accidentally in production). I thought that I could make the defaults opt-in by defining my own trait TestDefault and implement Default for any type that implements it. Then, one could access all the features of the standard Default trait using something like this

use TestDefault; // TestStruct (defined in my crate) implements TestDefault, thus also Default

let test_struct = TestStruct::default();

To clarify, I want to implement a foreign trait on local type, which should be allowed, but with an artificial layer of indirection to make it opt-in.

I've tried

pub trait TestDefault {
    fn test_default() -> Self;
}

impl Default for TestDefault {
    fn default() -> Self {
        Self::test_default()
    }
}

where the compiler complains that error[E0782]: trait objects must include the 'dyn' keyword, inserting it instead causes it to fail because error[E0038]: the trait 'TestDefault' cannot be made into an object.

Then I tried

impl<T> Default for T
where
    T: TestDefault,
{
    fn default() -> T {
        T::test_default()
    }
}

and got

error[E0210]: type parameter `T` must be used as the type parameter for some local type (e.g., `MyStruct<T>`)
   --> src/lib.rs:158:14
    |
158 |         impl<T> Default for T
    |              ^ type parameter `T` must be used as the type parameter for some local type
    |
    = note: implementing a foreign trait is only possible if at least one of the types for which it is implemented is local
    = note: only traits defined in the current crate can be implemented for a type parameter

which probably hints at the actual error, but I don't entirely understand it. Is there any way to do this? Or get opt-in default some other way?

kmdreko
  • 42,554
  • 6
  • 57
  • 106
  • Does this answer your question? [How do I implement a trait I don't own for a type I don't own?](https://stackoverflow.com/questions/25413201/how-do-i-implement-a-trait-i-dont-own-for-a-type-i-dont-own) – Chayim Friedman Jul 20 '22 at 09:55
  • @ChayimFriedman Thanks, but I don't think so. `TestStruct` would be defined in my crate. So I'd still be implementing a foreign trait on a local type which should be allowed, but with an artificial layer of indirection. – Sebastian Holmin Jul 20 '22 at 10:19
  • Then why not implement `Default` directly? Anyway, if you're using generics, you're implementing it for any type, not just those defined in your crate. – Chayim Friedman Jul 20 '22 at 10:24
  • The main point was that you only should have access to the default implementations when manually writing `use TestDefault`, since it would only be a convenience feature for tests and not a practical default. I took inspiration from how [extension traits](https://rust-lang.github.io/rfcs/0445-extension-trait-conventions.html#:~:text=Extension%20traits%20are%20a%20programming,need%20for%20a%20clear%20convention.) work, where you have to `use` them to get access to the associated methods. It wouldn't be generic though since I have to manually implement `TestDefault` for each of my structs. – Sebastian Holmin Jul 20 '22 at 10:57
  • Even if what you want was allowed, this would not be the case - the `Default` implementation will always be available as long as `Default` itself is. – Chayim Friedman Jul 20 '22 at 11:02

1 Answers1

3

You can use the #[cfg(test)] annotation to only enable the Default implementation when testing:

struct MyStruct {
    data: u32,
}

#[cfg(test)]
impl Default for MyStruct {
    fn default() -> Self {
        Self { data: 42 }
    }
}

fn main() {
    let s = MyStruct::default(); // FAILS
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let s = MyStruct::default(); // WORKS
        assert_eq!(s.data, 42);
    }
}
> cargo test
running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
> cargo run
error[E0599]: no function or associated item named `default` found for struct `MyStruct` in the current scope
  --> src/main.rs:13:23
   |
1  | struct MyStruct {
   | --------------- function or associated item `default` not found for this
...
13 |     let s = MyStruct::default(); // FAILS
   |                       ^^^^^^^ function or associated item not found in `MyStruct`
   |
   = help: items from traits can only be used if the trait is implemented and in scope
   = note: the following trait defines an item `default`, perhaps you need to implement it:
           candidate #1: `Default`
kmdreko
  • 42,554
  • 6
  • 57
  • 106
Finomnis
  • 18,094
  • 1
  • 20
  • 27
  • I had no idea you could use `#[cfg(test)]` on `impl` blocks! That's actually a really good solution to my specific problem, thanks! – Sebastian Holmin Jul 20 '22 at 11:34
  • That's what it does, it enables/disables the compilation of code depending on whether it's a test run :) – Finomnis Jul 20 '22 at 11:43