I recently got quite stuck on a bug in my project which boils down to this example:
use std::any::Any;
#[derive(Debug)]
pub struct Foo(pub u32);
fn boxit<T: 'static>(x: T) -> Box<dyn Any> {
Box::new(x)
}
fn unboxit<T: 'static>(b: Box<dyn Any>) -> T {
*b.downcast::<T>().unwrap()
}
fn main() {
let b = boxit(Foo);
let x: Foo = unboxit(b);
println!("{x:?}");
}
The footgun here is that once Foo was a ZST type and the program worked, but then someone added a field to it, and while the code kept compiling the semantics changed. Suddenly Foo in boxit(Foo) was a function fn() -> Foo and main panicked.
If it's a recurring issue, you could declare your unit structs as struct Foo(). Also, I'm not sure this qualifies as a semver issue, since adding fields to a public struct like that is already a breaking change.
It's still a semver breakage to go from a publically-constructable unit struct to a non-constructable non-unit struct, since old code attempting to create a unit struct is now broken. It's just that in this particular case, it's a runtime instead of comptime error.
if you want to guarantee a compile-time error instead of silently changing semantics when Foo's definition changes to add fields, you can write boxit(Foo {}) instead, which will error when anyone adds fields, but will keep working whenever Foo is a struct without fields (whether it's declared struct Foo;, struct Foo();, or struct Foo {}) as long as Foo is not #[non_exhaustive].