I've been bitten by this too and so have many other libraries. Auto-traits don't "escape" crates as far as I know, hence I was thinking it should be possible for individual crates to opt-out of auto-traits and instead require that they are listed manually in a derive like:
#[derive(Send)]
struct Model { }
I think this could be especially interesting for library authors which care a lot about semver stability.
Has this ever been considered? What are people's thoughts on this?
Sorry, I should have been more clear. With "escaping" I meant that whether a trait is implemented automatically or manually does not escape a crate. For example, it makes no difference to a user whether Send is implemented manually or implicitly via an auto-trait.
What I am suggesting is, to provide a mechanism (per crate, e.g. in Cargo.toml) to completely opt-out of auto traits and instead to explicitly implement those traits.
Currently manually implementing Send for example is unsafe and implementing UnwindSafe is not possible as far as I know.
We could change that and provide a derive like #[derive(Send)]. This would fail if a not all fields are Send. As a result, implementing Send for certain structs (which is part of the public API) would be an explicit decision by the developer. With auto traits, the struct is simply silently no longer Send which is a semver-hazard.
Long-story short: An option for crates to turn off auto-traits and require explicit implementations instead.
(This is an aside to your topic, but) it's not only possible, but doesn't require unsafe, to implement UnwindSafe. As such, it supplies no guarantees that unsafe can rely on and is effectively a lint, and not at all a promise of safety. (For these reasons some people find it quite misleading and would rather it be deprecated or made irrelevant altogether, as discussed in this issue.)
Before I give another wrong example, I am just going to stick to Send and Sync . These auto-traits easily become part of your public API without noticing and then it is a breaking change to remove them later, even if you just refactor internals.
I wonder: Does this really happen often that you "just refactor internals" and that causes somestruct or enum to become !Send or !Sync? I'm curious about real-life examples where this happened (not wanting to say it doesn't happen, I just lack the experience to picture cases where this happens). What's the usual "change" that makes these auto-traits be removed?
Regarding !Send, I'm thinking on FFI mostly, and also on std::sync::MutexGuard which is unfortunately !Send. I feel like Sync can often be fixed with adding an explicit Mutex where needed?
We specifically had wrapped all errors prior to this PR to be able to switch the protobuf implementation in a patch-release and had to find an awkward workaround because the error from the new library was not UnwindSafe.
I think the issue isn't so often that you refactor your own code but if you swap out an implementation detail, i.e. a one library for another, a lot of things can change.
If we wouldn't have used cargo semver-checks, we would have never noticed that breaking change.
On the whole, I think while convenient, auto-traits are quite the foot-gun because as library authors you end up committing to an API that you didn't intend to and you can easily break backwards-compatibility without knowing. The latter can be solved with cargo semver-checks.
Perhaps the former could be solved with a lint missing_send_implementation and the like? It would have to only target exported types. The assumption would be that in most cases, you want your types to be Send, UnwindSafe etc