Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-129 · Case file with fixtures · Case 101 of 694 · Compiler evidence

When derive(Clone) Adds an Unneeded Bound Around Arc<T>

A derived Clone implementation may expose T: Clone for a generic wrapper although Arc<T> is cloneable without that bound. A manual implementation can clone the handle and preserve the intended weak contract.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
targets with std::sync::Arc
Profiles
check, dev, release, test

Direct answer

What this Rust failure means

Why it happens
The generated Clone implementation places a conservative Clone bound on the generic parameter, which is stronger than the field operation actually needs.
First discriminating check
Compare the bound generated by derive with the field type's own Clone implementation before adding Clone to a domain type that should not need it.

The failing program derives Clone for a generic Shared<T> containing one Arc<T>. Calling clone() on Shared<NoClone> then fails because the generated implementation expects NoClone: Clone.

This is surprising because cloning an Arc<T> increments a reference count. It does not clone the T stored in the allocation.

Cloning a handle is not cloning its value

The Arc documentation describes atomically reference-counted shared ownership. Arc::clone(&value) creates another owning handle to the same allocation. Both handles observe the same T.

This operation is available without T: Clone. The value can be expensive, non-cloneable, or intentionally unique at the object level while still having several ownership handles.

I keep this distinction visible:

Arc<T>::clone       -> duplicate an ownership handle
T::clone            -> create another T value

They express different semantics and usually have very different costs.

Derive can expose a stronger generic contract

The derive reference explains that the attribute generates a trait implementation. For generic types, that generated code may include conservative parameter bounds.

In this case, the effective bound prevents Shared<T> from being cloned unless T itself implements Clone. The implementation is valid, but it is more restrictive than cloning the only field requires.

Adding #[derive(Clone)] to NoClone would satisfy the compiler. It would also suggest that the domain value has meaningful duplication semantics, which may be false. I do not modify the inner type only to accommodate an accidental wrapper bound.

The manual implementation clones exactly one field

The repaired program makes the desired operation explicit:

impl<T> Clone for Shared<T> {
    fn clone(&self) -> Self {
        Self { value: self.value.clone() }
    }
}

The body only clones Arc<T>, so the implementation needs no T: Clone. Shared<NoClone> now gains another handle to the same NoClone allocation.

I sometimes spell the field operation as Arc::clone(&self.value). This makes shared-handle cloning stand out during review and avoids implying that the inner domain object is duplicated.

This changes the API, not only compilation

Generic bounds determine which downstream types can use an abstraction. A hidden unnecessary bound may only appear when a user stores a file handle, mutex, callback, parser state, or another non-Clone value in the wrapper.

For library code, I add a small compile test with a deliberately non-cloneable type. It documents the promise that cloning the wrapper duplicates ownership rather than data:

struct NoClone;
let shared = Shared { value: Arc::new(NoClone) };
let another = shared.clone();

This prevents a later refactor back to derive from silently narrowing the public contract.

I also name methods carefully in documentation. clone_shared_handle communicates aliasing more clearly than a domain verb such as duplicate_job. If callers need an independent value later, that should be another operation with its own T: Clone bound. Keeping both operations separate prevents a cheap handle clone from being mistaken for a deep copy during a performance or concurrency review.

A shallow clone can still surprise callers

Removing the T: Clone bound is correct only if shared ownership is the intended meaning of Shared<T>::clone. Both wrapper values point to the same allocation. If T contains interior mutability, changes through one handle can be observed through the other.

Some APIs use Clone to mean an independent snapshot. An Arc field does not provide that. In such a design, requiring T: Clone and cloning the underlying value might be the honest implementation:

impl<T: Clone> Clone for Snapshot<T> {
    fn clone(&self) -> Self {
        Self { value: Arc::new((*self.value).clone()) }
    }
}

The correct bound follows semantic intent, not the shortest compiler fix.

My debugging sequence

When clone() is unavailable on a generic wrapper, I do this:

  1. Read which type parameter the diagnostic says lacks Clone.
  2. Identify whether the failure comes from a derived implementation.
  3. Check each field's actual Clone contract in its documentation.
  4. Decide whether clone should share, duplicate, or snapshot the underlying value.
  5. Write the narrow manual implementation when handle sharing is intended.
  6. Test with a non-Clone inner type and, separately, test aliasing behaviour.

The core lesson is not that derive is bad. Derive is excellent when its generated contract matches the abstraction. For ownership wrappers, I still inspect that contract, because cloning a pointer-like handle and cloning the pointee are not the same promise.