Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-518 · Case file with fixtures · Case 490 of 694 · Compiler evidence

Rust Trait Object Bounds Behind a Reference Need Parentheses

The plus operator in type bounds has low precedence. Parenthesise the complete dyn object behind a reference so ownership and the full bound set have one unambiguous structure.

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
Low-precedence bound composition and the outer reference create two type layers whose grouping cannot be recovered safely from whitespace.
First discriminating check
Parenthesise the complete dyn referent, then verify its principal, auto-trait, and lifetime bounds reflect how the object is actually stored and shared.

I wrote &'a dyn Send + Sync. Rust 1.98.1 reported an ambiguous + in the type and suggested parentheses. The compiler no longer labels this exact diagnostic E0178 in the fixture, but the official E0178 explanation documents the same precedence boundary.

The failing fixture needs to say whether the reference points to one object whose bounds are Send + Sync.

Plus combines bounds at low precedence

In a trait-object type, + combines a principal trait, auto traits, and lifetime bounds under the relevant rules. A leading reference operator creates another layer of type structure.

Without grouping, &dyn A + B can be read as adding B outside the borrowed type or inside the object. The grammar rejects the ambiguity rather than choosing by visual spacing.

The official E0178 page explains that parentheses are often required.

The repaired fixture groups the referent

The repaired fixture writes &'a (dyn Send + Sync). The reference lifetime belongs to the outer borrow, and the parenthesised referent is one trait object carrying both auto-trait bounds.

The fixture then borrows a u8, which satisfies both bounds. No runtime wrapper or allocation is introduced by the parentheses.

They clarify the type tree; they do not change representation by themselves.

Dyn is part of modern trait-object syntax

dyn makes the object boundary explicit. The object erases a concrete implementing type while retaining dynamic dispatch metadata and selected auto-trait and lifetime properties.

The trait-object Reference defines which bounds may be combined. Parentheses do not permit two unrelated non-auto principal traits when the object rules reject that composition.

Grouping solves precedence, not every trait-object validity constraint.

Auto traits communicate movement safety

Send and Sync are auto traits commonly attached to objects crossing thread or shared-reference boundaries. dyn Service + Send + Sync promises that erased implementors satisfy those properties.

Adding the bounds can make construction fail for implementations containing non-thread-safe state. Removing them only to compile may make the object unusable where the application needs to move or share it.

I derive the bound set from how the object flows through the system.

A type alias can keep grouping readable

Long object types repeated across signatures are easy to parenthesise inconsistently. An alias such as type ServiceObject = dyn Service + Send + Sync lets references use &ServiceObject.

Aliases should preserve important object lifetimes explicitly. They improve readability but do not create new types or alter auto-trait requirements.

I use them when the bound set is a stable architectural contract.

Diagnostics evolve even when the principle remains

The Atlas fixture records the actual Rust 1.98.1 text rather than pretending it still emits E0178. Searchers may encounter the historic code, the current unnumbered message, or a parser suggestion.

Keeping both terms in metadata connects the old search language to current evidence. The mechanism—low-precedence + requiring unambiguous grouping—remains the same.

Lifetimes can belong inside the object too

&'a (dyn Service + 'b) carries two relationships: the outer reference is usable for 'a, and the erased implementor satisfies object lifetime bound 'b. They may be related but are not interchangeable.

When elision is sufficient, I keep syntax smaller. When an object borrows data or crosses API layers, I write the intended bound and explain which owner it follows. Adding 'static to the object can reject borrowed implementations even though the reference itself is short-lived. Parentheses make this nested structure readable enough to reason about.

Smart pointers need the same structural reading

Box<dyn Service + Send> naturally groups bounds inside its angle brackets. References have no brackets of their own, which is why parentheses matter more. For Arc, Pin, and nested references, I sketch the type from outside inward: owner or borrow first, then the complete referent. This prevents a formatting fix from obscuring ownership semantics.

My ambiguous-plus checklist

  • What complete type should the reference or pointer contain?
  • Which bounds belong inside the dyn object?
  • Are parentheses making the type tree explicit?
  • Does the object have a valid principal-trait composition?
  • Are Send and Sync required by actual movement or sharing?
  • Did removing a bound only move failure to another boundary?
  • Would a type alias make repeated object bounds safer to read?
  • Am I matching the current compiler wording in evidence?

The core principle is that bound composition and pointer composition are separate layers. Parentheses show exactly which + bounds form the trait object behind the reference.