Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-496 · Case file with fixtures · Case 468 of 694 · Compiler evidence

A Rust Type Parameter Can Relax Sized Only Once

Type parameters are Sized by default. The special ?Sized syntax removes that implicit default once; repeating it adds no capability and is rejected as a duplicate relaxed bound.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all Rust targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
The first ?Sized already removes the default Sized requirement, so a second copy adds no supported semantic change and remains duplicate special syntax.
First discriminating check
Inspect expanded bounds for repeated ?Sized, keep exactly one only when unsized callers are intentional, and validate storage and method operations for DST support.

I wrote T: ?Sized + ?Sized on one type parameter. Rust emitted E0203 because the special relaxation of the default Sized bound may appear only once.

The failing fixture is deliberately mechanical. Real code reaches this shape after combining generated bounds, expanding aliases, or merging two constraints that each believed they owned ?Sized.

Type parameters normally have an implicit Sized bound

Most generic type parameters behave as though T: Sized were written. This means values of T have a compile-time-known size and can participate in layouts that require it.

The Rust Reference section on ?Sized explains that the question-mark form relaxes this implicit default for type parameters and associated types. It does not mean “maybe implements Sized” in the way an ordinary optional feature might read.

I translate T: ?Sized as “do not require the usual implicit Sized bound here.”

Relaxing one default twice changes nothing

Once Sized has been relaxed, writing the same relaxation again cannot widen the admitted type set. Rust rejects the duplicate rather than silently accepting redundant special syntax.

The official E0203 page shows the direct repair: keep one ?Sized. The repaired fixture does exactly this.

This case is simpler than conflicting positive and negative logic. There is one implicit default and one supported way to relax it.

Unsized values still need a sized container boundary

Allowing T: ?Sized does not make every placement of T valid. Dynamically sized types such as str, slices, and trait objects do not have a compile-time-known size by themselves.

The Reference on dynamically sized types describes their restrictions. In a struct, an unsized field must be the final field. Values are normally handled behind references or pointer-like types that have a known representation.

I separate two questions: may the parameter be unsized, and is this particular storage position valid for an unsized value?

Adding ?Sized should be driven by an API need

A borrowed wrapper may want to accept both String and str, or both a concrete implementor and a trait object. In such cases ?Sized can widen useful APIs without allocation.

But adding it everywhere increases the set of types every method body must support. Operations that move T by value, compute its size directly, or depend on Self: Sized may stop working.

I add the relaxation at the narrowest abstraction that genuinely benefits from unsized inputs.

Bound composition can create the duplicate

A macro might collect user bounds and then append ?Sized. If the user already supplied it, the expanded parameter contains two copies. Another generator may combine a standard wrapper template with a field-specific template.

I normalise bounds by semantic identity before emitting tokens. The fix belongs in the composition step, not in one generated output file. Compile tests should cover a default parameter and an explicitly unsized parameter.

Because ?Sized is special syntax, a general trait-bound deduplicator may otherwise overlook it.

Do not replace the duplicate with Sized blindly

Changing one occurrence to Sized makes the declaration require a sized type again. That may compile, but it reverses the intended API widening.

Removing both occurrences also restores the implicit Sized requirement. The correct E0203 repair keeps exactly one relaxation when unsized support is intended, or removes the feature deliberately when it is not.

I check representative callers with str, [T], or dyn Trait before deciding.

Associated types are another valid location

Traits may relax the implicit bound on an associated type with a declaration such as type Target: ?Sized. This supports patterns similar to dereferencing into str or slices. The same rule applies: write the relaxation once and ensure users access the result through a reference or another sized handle.

I check both the generic parameter list and associated-type declarations when a macro combines bounds. Fixing one duplicate does not prove another generated slot is clean.

My E0203 checklist

  • Does the parameter contain ?Sized more than once after expansion?
  • Was the duplicate introduced by a macro or merged bound list?
  • Does the API truly need to accept dynamically sized types?
  • Is the unsized value kept behind a pointer or valid final field?
  • Would removing both bounds accidentally restore implicit Sized?
  • Does any method move T by value or require its size?
  • Can the relaxation live on a narrower parameter or method?
  • Are representative sized and unsized callers compiled in tests?

The core principle is that ?Sized removes one implicit restriction. It should appear once, with a concrete reason, and the surrounding layout and operations must still support the wider set of types.