Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Summary

Support restricting implementation of individual methods within traits, using the existing unused final keyword.

Motivation

When defining a trait, the trait can provide optional methods with default implementations, which become available on every implementation of the trait. However, the implementer of the trait can still provide their own implementation of such a method. In some cases, the trait does not want to allow implementations to vary, and instead wants to guarantee that all implementations of the trait use an identical method implementation. For instance, this may be an assumption required for correctness.

This RFC allows restricting the implementation of trait methods.

This mechanism also faciliates marker-like traits providing no implementable methods, such that implementers only choose whether to provide the trait and never how to implement it; the trait then provides all the method implementations.

One example of a trait in the standard library benefiting from this: Error::type_id, which has thus far remained unstable because it’s unsafe to override. This RFC would allow stabilizing that method so users can call it, without permitting reimplementation of it.

Another would be the Read::read_buf_exact method. Making this final would allow callers to rely on its implementation to be correct, while keeping the function safe to call. Without this, callers using unsafe code must defend against the possibility of an incorrect read_buf_exact implementation (e.g. returning Ok(()) without filling the buffer) to avoid UB.

Explanation

When defining a trait, the definition can annotate methods or associated functions to restrict whether implementations of the trait can define them. For instance:

trait MyTrait: Display {
    final fn method(&self) {
        println!("MyTrait::method: {self}");
    }
}

A method or associated function marked as final must have a default body.

When implementing a trait, the compiler will emit an error if the implementation attempts to define any method or associated function marked as final, and will emit a suggestion to delete the implementation.

In every other way, an final method or associated function acts identically to any other method or associated function, and can be invoked accordingly:

fn takes_mytrait(m: &impl MyTrait) {
    m.method();
}

Note that in some cases, the compiler might choose to avoid placing a final method in the trait’s vtable, if the one-and-only implementation does not benefit from monomorphization.

Note that removing a final restriction is a compatible change. (Removing a default implementation remains a breaking change.)

Reference-level explanation

At runtime, a final fn behaves exactly the same as a fn.

Removing final may be a non-breaking change. (If final was preventing implementation to prevent a soundness issue, though, this would require additional care.)

Adding final is a breaking change, unless the trait already did not allow third-party implementations (such as via a sealed trait).

At compile-time, a method declared as final fn in a trait must have a provided body, and cannot be overridden in any impl, even an impl in the same crate or module.

final fn cannot be combined with default fn.

final is only allowed in trait definitions. final is not allowed on impls or their items, non-trait functions, or extern blocks.

A final fn never prevents a trait from having dyn-compatibility; the trait can remain dyn-compatible as long as all non-final methods support dyn-compatibility. This also means that a final fn can always be called on a dyn Trait, even if the same method as a non-final fn would not have been dyn-compatible.

Drawbacks

As with any language feature, this adds more surface area to the language.

Rationale and alternatives

Instead of or in addition to this, we could allow inherent impl blocks for a Trait (e.g. impl Trait { ... } without for Type). People today already occasionally write impl dyn Trait blocks, since dyn Trait is a type and supports inherent impl blocks; this change would allow generalizing such blocks by deleting the dyn. This has the potential for conceptual complexity or confusion for new users, as well as potentially affecting the quality of diagnostics. (It also used to have a meaning in Rust 2015: the same meaning impl dyn Trait now has.) However, it would provide orthogonality, and an interesting conceptual model.

Rather than using final, we could use the impl(visibility) syntax from RFC 3323. This would allow more flexibility (such as overriding a method within the crate but not outside the crate), and would be consistent with other uses of RFC 3323. On the other hand, such flexibility would come at the cost of additional complexity. We can always add such syntax for the more general cases in the future if needed; see the future possibilities section.

Rather than using final, we could use #[final]. This concept is somewhat similar to “final” methods in other languages, and we already have the final keyword reserved so we could use either an attribute or a keyword.

It’s possible to work around the lack of this functionality by placing the additional methods in an extension trait with a blanket implementation. However, this is a user-visible API difference: the user must import the extension trait, and use methods from the extension trait rather than from the base trait.

Prior art

This feature is similar to final methods in Java or C++.

It’s also similar to sealed in C#, where sealed class is something from which you can’t derive and a base class can use sealed on a method to say derived classes can’t override it.

Unresolved questions

None yet.

Future possibilities

final methods do not need to appear in a trait’s vtable. However, if a method is dyn-compatible, and if it would benefit from monomorphization, we could optionally put it in the trait’s vtable, perhaps with an explicit option to do so.

We could allow final fn methods on #[marker] traits, which are currently not allowed to have any methods (because they can’t allow different implementations in different impls).

As mentioned in the alternatives section, we could allow inherent impl blocks for a Trait (e.g. impl Trait { ... } without for Type). People today already occasionally write impl dyn Trait blocks, since dyn Trait is a type and supports inherent impl blocks; this change would allow generalizing such blocks by deleting the dyn.

When evaluating possible future syntaxes such as impl Trait { ... } blocks, we should take into account:

  • The conceptual model we want to present to users
  • Whether we anticipate user confusion due to the former meaning of this syntax in Rust 2015 (prior to the move from Trait to dyn Trait to write trait objects)
  • Any effect on diagnostic quality
  • Whether an additional syntax adds excessive implementation complexity
  • How much we want the benefit of allowing impl dyn Trait blocks to be generalized by deleting the dyn

We could add additional flexibility using the restriction mechanism defined in RFC 3323, using syntax like impl(crate) to restrict implementation of a method or associated function outside a crate while allowing implementations within the crate. (Likewise with impl(self) or any other visibility.)

We could theoretically allow final restrictions on associated consts and types, as well. If this is simple to implement, we should implement it for all items that can appear in a trait simultaneously; if it proves difficult to implement, we should prioritize methods.

We could support some syntax (e.g. impl(unsafe)), to make a method safe to call, but unsafe to override. This would allow the implementation to be trusted, so that unsafe code can rely on it rather than defending against incorrect implementations.

We could integrate this with stability markers, to stabilize calling a method but keep it unstable to implement.