# [derive(Clone)] Is Broken

128 pointsposted 5 days ago
by RGBCube

49 Comments

lidavidm

a day ago

Someone on the issue they made explained why Clone is "broken": https://github.com/JelteF/derive_more/issues/490#issuecommen...

Which links to this blog post explaining the choice in more detail: https://smallcultfollowing.com/babysteps/blog/2022/04/12/imp...

samsartor

a day ago

I have a crate with a "perfect" derive macro that generates where clauses from the fields instead of putting them on the generic parameters. It is nice when it works, but yah cyclical trait matching is still a real problem. I wound up needing an attribute to manually override the bounds whenever they blow up: https://docs.rs/inpt/latest/inpt/#bounds

sbt567

a day ago

from Niko's post:

> In the past, we were blocked for technical reasons from expanding implied bounds and supporting perfect derive, but I believe we have resolved those issues. So now we have to think a bit about semver and decide how much explicit we want to be.

user

a day ago

[deleted]

loa_in_

a day ago

Automatically deriving Clone is a convenience. You can and should write your own implementation for Clone whenever you need it and automatically derived implementation is insufficient.

josephg

a day ago

But this issue makes it confusing & surprising when an automatically derived clone is sufficient and when its not. Its a silly extra rule that you have to memorise.

By the way, this issue also affects all of the other derivable traits in std - including PartialEq, Debug and others. Manually deriving all this stuff - especially Debug - is needless pain. Especially as your structs change and you need to (or forget to) maintain all this stuff.

Elegant software is measured in the number of lines of code you didn't need to write.

eqvinox

a day ago

The entirety of any programming language is a convenience. You could just write your code in assembly; I don't think your argument is "automatic". Question is, how much does this particular convenience matter?

The fact that people are writing blog posts and opening bugs about it (and others in the comments here recount running into the issue) seems to indicate this particular convenience matters.

benreesman

a day ago

Haskell/GHC gets this right without any hand-wringing and about 3-4x the practical historical burden and without relying on the runtime.

The Rust community is very adamant as a general thing that "you're holding it wrong" when people complain about e.g. BDSM passing semantics, but it's also got REIR/RIIR, which is kinda like "you're holding it wrong" for programming.

These two things together are a category error.

xorvoid

a day ago

Am I the only one who thinks this is perfectly fine?

The requirements for derive Clone are clearly defined. As with much in Rust, the type signature drives things, rather than the function body (contrast with C++ generics).

Occasionally, this results in perfectly reasonable code getting rejected. Such is the case with all static languages (by definition).

But in the few cases that happen, the solutions are quite straightforward. So, I don’t feel like it’s justified to add more complication to the language to deal with a few small corner cases.

bobbylarrybobby

14 hours ago

If you write code manually, you can forget to update it in the future. For instance, a manual implementation of PartialEq might become stale if you add new fields in the future. If you could automatically generate the implementation, and simply guide the macro to use non-default behavior (e.g. skip a field, or use a more complicated trait bound on a generic type) then you can have the advantages of generated code without the disadvantages. Seems worth trying for, IMO.

jhugo

a day ago

> we cannot just require all generic parameters to be Clone, as we cannot assume they are used in such a way that requires them to be cloned.

No, this is backwards. We have to require all generic parameters are Clone, as we cannot assume that any are not used in a way that requires them to be Clone.

> The reason this is the way it is is probably because Rust's type system wasn't powerful enough for this to be implemented back in the pre-1.0 days. Or it was just a simple oversight that got stabilized.

The type system can't know whether you call `T::clone()` in a method somewhere.

chrismorgan

a day ago

> The type system can't know whether you call `T::clone()` in a method somewhere.

It’s not about that, it’s about type system power as the article said. In former days there was no way to express the constraint; but these days you can <https://play.rust-lang.org/?gist=d1947d81a126df84f3c91fb29b5...>:

  impl<T> Clone for WrapArc<T>
  where
      Arc<T>: Clone,
  {
      …
  }

enricozb

a day ago

For structs, why couldn't rust check the necessary bounds on `T` for each field to be cloned? E.g. in

    #[derive(Clone)]
    struct Weird<T> {
      ptr: Arc<T>,
      tup: (T, usize)
    }

for `ptr`, `Arc<T>: Clone` exists with no bound on `T`. But for `tup`, `(T, usize): Clone` requires `T: Clone`.

Same thing for other derives, such as `Default`.

berkes

a day ago

> The type system can't know whether you call `T::clone()` in a method somewhere.

Why not?

almostdeadguy

a day ago

All the #[derive(Clone)] does is generate a trait impl of Clone for the struct, which itself can be bounded by trait constraints. It doesn't have to know that every use of the struct ensures generic parameters have to/don't have to be Clone. It doesn't have to make guarantees about how the struct is used at all.

It only needs to provide constraints that must hold for it to call clone() on each field of the struct (i.e. the constraints that must hold for the generated implementation of the fn clone(&self) method to be valid, which might not hold for all T, in which case a Struct<T> will not implement Clone). The issue this post discusses exists because there are structs like Arc<T> that are cloneable despite T not being Clone itself [1]. In a case like that it may not be desirable to put a T: Clone constraint on the trait impl, because that unnecessarily limits T where Struct<T>: Clone.

[1]: https://doc.rust-lang.org/std/sync/struct.Arc.html#impl-Clon...

moomin

a day ago

Haskell does this. If you derive Eq, it puts a condition on the generic parameter(s) requiring them to be Eq as well. Then if you use it with something that doesn’t implement Eq, your generic type doesn’t either.

It helps if you can express these preconditions in the first place, though.

Haskell has, like-for-like, a better type system than Rust.

That said, Rust is just enough Haskell to be all the Haskell any systems programmer ever needed, always in strict mode, with great platform support, a stellar toolkit, great governance, a thriving ecosystem and hype.

Lots of overlap in communities (more Haskell -> Rust than the other way IMO) and it's not a surprise :)

evertedsphere

a day ago

that's incorrect

haskell implements the "perfect derive" behaviour that the blog post is complaining about rust's lack of. the constraint is only propagated to the field types when using a haskell derive, not the parameters themselves (as in rust)

so the following compiles just fine:

    data Foo -- not Eq

    data Bar a = Bar 
      deriving Eq

    f :: Eq (Bar Foo) => ()
    f = ()

user

a day ago

[deleted]

hyperbrainer

a day ago

> we cannot just require all generic parameters to be Clone, as we cannot assume they are used in such a way that requires them to be cloned.

I don't understand what "used in such a way requires them to be cloned" means. Why would you require that?

xvedejas

a day ago

Right now the derive macro requires `T` be `Clone`, but what we actually want to require is only that each field is clone, including those that are generic over `T`. eg `Arc<T>` is `Clone` even though `T` isn't, so the correct restriction would be to require `Arc<T>: Clone` instead of the status quo which requires `T: Clone`

tinco

a day ago

That's the crux of the article. There's no good reason for this requirement, at least none that arise in the article, so the author concludes it must be a mistake.

I think it's a bit cynical that it would cost at least 4 years for this change to be admitted into the compiler. If the author is right and there is really no good reason for this rule, and I agree with the author in seeing no good reason, then it seems like something that could be changed quite quickly. The change would allow more code to compile, so nothing would break.

The only reason I could come up with for this rule is that for some other reason allowing non complying type parameters somehow makes the code generation really complex and they therefore postponed the feature.

rocqua

a day ago

A type might have a generic parameter T, but e.g. use it as a phantom marker.

Then even if T isn't cloneable, the type might still admit a perfectly fine implementation of clone.

qwertox

a day ago

LLMs are broken, too:

> "Of course. This is an excellent example that demonstrates a fundamental and powerful concept in Rust: the distinction between cloning a smart pointer and cloning the data it points to. [...]"

Then I post the compiler's output:

> "Ah, an excellent follow-up! You are absolutely right to post the compiler error. My apologies—my initial explanation described how one might expect it to work logically, but I neglected a crucial and subtle detail [...]"

Aren't you also getting very tired of this behavior?

the_mitsuhiko

a day ago

> Aren't you also getting very tired of this behavior?

The part that annoys me definitely is how confident they all sound. However the way I'm using them is with tool usage loops and so it usually runs into part 2 immediately and course corrects.

rfoo

a day ago

TBH I'm tired of only the "Ah, an excellent follow-up! You are absolutely right <...> My apologies" part.

codedokode

a day ago

Languages like Rust and C seem to be too complicated for them. I also asked different LLMs to write a C macro or function that creates a struct and a function to print it (so that I don't have to duplicate a field list) and it generates plausible garbage.

ramon156

a day ago

You should check Twitter nowadays, people love this kind of response. Some even use it as an argument

renewiltord

a day ago

Haha, I encountered the opposite of this when I did a destructive thing recently but first asked Gemini, then countered it saying it’s wrong and it insisted it was right. So the reality they encountered is probably that: it either is stubbornly wrong or overly obsequious with no ability to switch.

My friend was a big fan of Gemini 2.5 Pro and I kept telling him it was garbage except for OCR and he nearly followed what it recommended. Haha, he’s never touching it again. Every other LLM changed its tune on pushback.

user

a day ago

[deleted]

user

a day ago

[deleted]

kzrdude

a day ago

The only thing that needs change with derive(Clone) is to add an option to it so that you easily can customize the bounds. Explicitly.

m3talsmith

a day ago

Just looking at the examples, you can tell that they wouldn't compile: the other structs passed in don't derive the trait as well, nor implement it. It's really simple, not broken.

bloppe

a day ago

I don't see how "the hard way" is a breaking change. Anybody got an example of something that works now but wouldn't work after relaxing that constraint?

yuriks

a day ago

It relaxes the contract required for an existing type with derive(Clone) to implement Clone, which might allow types in existing code to be cloned where they couldn't before. This might matter if precluding those clones is important for the code, e.g. if there are safety invariants being maintained by Type<T> only being clonable if T is clone.

csomar

a day ago

Derive Clone is not broken. It is basic. I’d say this is a good area for a dependency but not the core Rust functionality. Keep derive simple and stupid, so people can learn they can derive stuff themselves. It also avoids any surprises.

josephg

a day ago

I disagree. I think the current behaviour is surprising. The first time I ran into this problem, I spent half an hour trying to figure out what was wrong with my code - before eventually realising its a known problem in the language. What a waste of time.

The language & compiler should be unsurprising. If you have language feature A, and language feature B, if you combine them you should get A+B in the most obvious way. There shouldn't be weird extra constraints & gotchas that you trip over.

DougBTX

a day ago

Agreed, a screwdriver isn’t broken just because it isn’t a good hammer. The title seems misleading, I was expecting a bug, memory unsafe etc.

Allowing more safe uses seems OK to me, but obviously expanding the functionality adds complexity, so there’s a trade off.

atemerev

a day ago

I mean, how this is not core Rust functionality if you need clone for many things unless you want to fight the borrow checker for yet another day?

xupybd

a day ago

Amazing site from someone so young

PontingClarke

21 hours ago

Great write-up! It’s a solid reminder that #[derive(Clone)] can introduce subtle behavior in deeply nested or generic types. Automation is helpful—but shouldn't replace carefully reviewing your code's intent. Thanks for bringing attention to this!

exfalso

a day ago

Eh. It's a stretch to call it "broken"

tucnak

a day ago

A bit off-topic, but every time I read some sophisticated Rust code involving macros, I cannot help but think that something went wrong at some point. The sheer complexity far outpaces that of C++, and even though I'm sure they would call C++ on undefined behaviour (and rightfully so) it seems less of it has to do with memory and thread-safety, and moreso with good old "C++ style" bloat: pleasing all, whilst pleasing none. Rust doesn't seem worthwhile to learn, as in a few years time C++ will get memory safety proper, and I could just use that.

Maybe this is an improvement on templates and precompiler macros, but not really.

junon

a day ago

None of this has to do with the complexity of macros.

And no, sorry, the complexity of C++ templates far outweighs anything in Rust's macros. Templates are a turing complete extension of the type system. They are not macros or anything like it.

Rust macro rules are token-to-token transformers. Nothing more. They're also sanitary, meaning they MUST form valid syntax and don't change the semantics in weird ways like C macros can.

Proc-macros are self-standing crates with a special library type in the crate manifest indicating as such, and while they're not "sanitary" like macros rules, they're still just token to token transformers that happen to run Rust code.

Both are useful, both have their place, and only proc macros have a slight developer experience annoyance with having to expand to find syntax errors (usually not a problem though).

coldtea

a day ago

>The sheer complexity far outpaces that of C++

Not even close. Rust Macros vs C++ templates is more like "Checkers" vs "3D-chess while blindfolded".

>Rust doesn't seem worthwhile to learn, as in a few years time C++ will get memory safety proper

C++ getting "memory safety proper" is just adding to the problem that's C++.

It being a pile of concepts, and features, and incompatible ideas and apis.

j-krieger

a day ago

I write both for a decade now and I disagree immensely. If Dante knew of the horrors of CPP template compiler errors he would've added an eighth ring.

I'm convinced that the only reason anyone's still using C++ is that they refuse to learn any other language, and the features they want may get added eventually. C++ is an absolute mess.

Surac

a day ago

it seems i have a personal dislike for rust syntax. i think non of the code should compile because they are just ugly :)