在学习rust的模板的时候, 我遇到了一个奇怪的问题. 就是关于derive(Clone)这个派生宏的. 有点意思, 跟大家分享一下.

问题来由

我们先来看这段代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::rc::Rc;
trait MayNotClone{}

#[derive(Clone)]
struct CloneByPtr<T> where T: MayNotClone{
    item: Rc<T>
}

trait MustBeClone: Clone {}
impl <T> MustBeClone for CloneByPtr<T> where T:MayNotClone{}

代码本身很简单, 首先声明一个trait叫做MayNotClone, 它不一定满足Clone这个trait, 然后, 再声明一个名为MustBeClone的trait, 这个trait必须实现Clone. 到这里, 一切正常, 对吧?

接下来, 就开始有点奇怪了, 当我们尝试为CloneByPtr这个结构体实现MustBeClone时, 编译器就会报错:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
PS C:\Users\rivers\Desktop\example> cargo build
   Compiling example v0.1.0 (C:\Users\rivers\Desktop\example)
error[E0277]: the trait bound `T: Clone` is not satisfied
 --> src\main.rs:9:10
  |
9 | impl <T> MustBeClone for CloneByPtr<T> where T:MayNotClone{}
  |          ^^^^^^^^^^^ the trait `Clone` is not implemented for `T`
  |
note: required because of the requirements on the impl of `Clone` for `CloneByPtr<T>`
 --> src\main.rs:4:10
  |
4 | #[derive(Clone)]
  |          ^^^^^
note: required by a bound in `MustBeClone`
 --> src\main.rs:8:20
  |
8 | trait MustBeClone: Clone {}
  |                    ^^^^^ required by this bound in `MustBeClone`
  = note: this error originates in the derive macro `Clone` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider further restricting this bound
  |
9 | impl <T> MustBeClone for CloneByPtr<T> where T:MayNotClone + std::clone::Clone{}
  |                                                            +++++++++++++++++++

For more information about this error, try `rustc --explain E0277`.
error: could not compile `example` due to previous error

编译器提示得很清晰, MustBeClone这个trait要求实现它的结构体必须满足trait, 但是T(也就是那个MayNotClone)不满足Copy的trait, 请考虑为T添加一个clone trait的限制.

我不是加了#[derive(Clone)]么, 怎么还不好使.

这个就有点无理取闹了啊. 这个CloneByPtr明明持有的是T的指针, 你指针能复制就好了嘛, 要指针指向的值能复制做干嘛.

我们来看一下#[derive(Clone)]吧!

https://doc.rust-lang.org/std/clone/trait.Clone.html#derivable

根据官方文档说, This trait can be used with #[derive] if all fields are Clone. 如果一个struct的全部field都是Clone的, 那这个宏可以让这个struct也变为Clone的.

那我这个没毛病啊, std::rc::Rc不是实现了Clone么. 它直接生成item.clone()不就完事了么.

那问题出在哪了呢? 我觉得应该还是在于derive(Clone)上, 我们来研究一下这个宏, 看看怎么回事.

首先, 让我们先考虑一下, 这个宏都干了什么. 如果不用它, 我们自己手动实现Clone, 应该是什么样子呢? 下面我们以一个比较简单的struct举个栗子

1
2
3
4
struct ImStruct{
    a: i32,
    b: Vec<i32>
}

如果为它实现Clone那么就应该是

1
2
3
4
5
6
7
8
impl Clone for ImStruct {
  fn clone(&self) -> Self {
    Foo {
      a: self.a.clone(),
      b: self.b.clone(),
    }
  }
}

那如果加入泛型呢? 像这样:

1
2
3
4
5
struct ImGenericStruct<T, U> {
  a: u32,
  b: Rc<T>,
  c: U,
}

那如果为它实现Clone就应该是:

1
2
3
4
5
6
7
8
9
impl<T, U> Clone for ImGenericStruct<T, U> {
  fn clone(&self) -> Self {
    Foo {
      a: self.a.clone(),
      b: self.b.clone(),
      c: self.c.clone(), // 这里8行, C类型不一定
    }
  }
}

所以如果想要为这个struct实现Clone, 就必须确保U是Clone的:

1
2
3
4
5
struct ImGenericStruct<T, U: Clone> {
  a: u32,
  b: Rc<T>,
  c: U,
}

到这里, 一切正常, 对吧? 那#[derive(Clone)]问题出在哪了呢? 这个简单, 我们直接看它生成了什么代码就可以了. 比如gcc,clang, 有一个-E参数, 可以显示模板,宏展开后的代码. rust也有这个功能, 不过需要使用nightly版本的才可以启用此特性.

1
2
3
rustup toolchain install nightly
cargo install cargo-expand
rustup default nightly #这里将rust暂时切换为nightly的版本, 记得之后改回去

cargo-expand

我们以上面 struct ImGenericStruct<T, U> 为例

1
2
3
4
5
6
#[derive(Clone)]
struct ImGenericStruct<T, U> {
  a: u32,
  b: Rc<T>,
  c: U,
}

其展开代码为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
struct ImGenericStruct<T, U> {
    a: u32,
    b: Rc<T>,
    c: U,
}
#[automatically_derived]
impl<T: ::core::clone::Clone, U: ::core::clone::Clone> ::core::clone::Clone
for ImGenericStruct<T, U> {
    #[inline]
    fn clone(&self) -> ImGenericStruct<T, U> {
        ImGenericStruct {
            a: ::core::clone::Clone::clone(&self.a),
            b: ::core::clone::Clone::clone(&self.b),
            c: ::core::clone::Clone::clone(&self.c),
        }
    }
}

由此可见, derive(Clone)画蛇添足地为T添加了Clone限制. emm, 这是rust的宏自身的限制, 它能做到读取代码的token流用来自动生成一些其他代码, 但是应该还不具备与编译器交互的能力, 也就没办法在宏展开时做出完善的类型检查, 所以只能简单粗暴地为里面出现的每一个模板参数都直接加上Clone的限制… 也许有一些其他的trait能避开这个问题, 不过直接手动实现一下, 也是一个可以考虑的方案.