首页
/ Rust 编译器 E0184 解析:为什么实现了 `Drop` 的类型禁止再实现 `Copy`

Rust 编译器 E0184 解析:为什么实现了 `Drop` 的类型禁止再实现 `Copy`

2026-09-06 17:33:07作者:范靓好Udolf

E0184 是 rustc 在一致性检查阶段抛出的错误:某个类型同时实现了 CopyDrop,这种组合被明确禁止,因为它与 Rust 的值语义相冲突并可能引发内存不安全。本文以 E0184 官方错误码文档 为主体,结合 rustc 源码中实际的检查逻辑与 UI 测试用例,完整讲解该错误的触发条件、报错形态、禁止原因(内存安全动机)以及常见规避方案,帮助读者在遇到 E0184 时快速定位问题并给出正确的修法。

一、E0184 错误码:触发场景与报错形态

E0184 文档 给出的触发场景是一个"反例":给一个类型 derive(Copy),同时又为它实现了 Drop

#[derive(Copy)]
struct Foo; // error!

impl Drop for Foo {
    fn drop(&mut self) {
    }
}

用当前仓库的 rustc 编译上述代码时,会得到如下的完整诊断输出(该输出即来自仓库中的期望输出文件 E0184.stderr):

error[E0184]: the trait `Copy` cannot be implemented for this type; the type has a destructor
  --> E0184.rs:2:8
   |
LL | #[derive(Copy)]
   |          ---- in this derive macro expansion
LL | struct Foo;
   |        ^^^ `Copy` not allowed on types with destructors
   |
note: destructor declared here
  --> E0184.rs:5:5
   |
LL |     fn drop(&mut self) {
   |     ^^^^^^^^^^^^^^^^^^

几个值得注意的细节:

  • 错误主消息是 "the trait Copy cannot be implemented for this type; the type has a destructor"(该类型有析构函数,因此不能实现 Copy);
  • 主诊断的 primary_span 指向 Copy 实现(含 derive 宏展开处)上的类型本身,标签为 "Copy not allowed on types with destructors";
  • 同时附带一个 note,把 Dropdrop 函数声明位置标注出来("destructor declared here"),方便读者一眼看到冲突的两个实现分别在哪里。

仓库中对应的最小复现用例是 E0184.rs,内容与 E0184 文档中的反例一致(#[derive(Copy)] struct Foo; + impl Drop for Foo),并带有 //~ ERROR E0184 断言,作为该错误码的回归测试。

二、为什么禁止 Copy + Drop 共存

E0184 文档给出的官方解释是:

Explicitly implementing both Drop and Copy trait on a type is currently disallowed. This feature can make some sense in theory, but the current implementation is incorrect and can lead to memory unsafety (see issue #20126), so it has been disabled for now.

即:同时显式实现 DropCopy 目前在理论上有意义,但当时的实现存在缺陷、可能带来内存不安全,因此被禁用。这与 Rust 语言层面的语义冲突是一致的,可以从两个 trait 各自承诺的语义来理解:

  1. Copy 的语义:类型为"位拷贝"语义。复制一个 Copy 值只是逐字节复制,源值在复制之后依然有效且可用,不存在"移动"概念。其 trait 定义位于标准库 clone.rs 中(CopyClone 的超 trait,要求"可零成本逐位复制")。
  2. Drop 的语义:类型为"需要析构"语义。值被移动(move)或离开作用域时,编译器会在原位置调用一次 drop,做释放资源、归还锁等操作。其 trait 定义位于 drop.rs 中。

两者的组合在语义上直接矛盾:

  • 若允许位拷贝且源值仍然有效,那么"移动即触发析构"的规则就会失效——一次拷贝可能留下多个"活值",析构何时触发、触发几次都无法确定;
  • 若在拷贝时立即对源值调用 drop,则 Copy 承诺的"源值复制后仍有效"被破坏,后续对源值的访问将读写已被析构的内存。

E0184 文档明确指出,历史上这一组合的实现是不正确的,会导致内存不安全(memory unsafety),所以被禁用。这也是为什么 rustc 把这条检查放在了编译期:与其让程序在运行期出现重复释放(double-free)或悬垂访问,不如在编译阶段就给出 E0184 错误。

三、编译器侧的检查实现:从诊断定义到检查调用链

从源码结构看,E0184 的报错由三部分协作完成:诊断结构定义、Copy 实现的合法性判定函数、以及一致性检查阶段的调度。

3.1 诊断定义:CopyImplOnTypeWithDtor

E0184 对应的诊断结构体定义在 diagnostics.rs 中:

#[derive(Diagnostic)]
#[diag("the trait `Copy` cannot be implemented for this type; the type has a destructor", code = E0184)]
pub(crate) struct CopyImplOnTypeWithDtor {
    #[primary_span]
    #[label("`Copy` not allowed on types with destructors")]
    pub span: Span,
    #[note("destructor declared here")]
    pub impl_: Span,
}

两个字段正好对应上文报错输出中的两部分:span 是主诊断跨度(落在 Copy 实现的自身类型上,附带 "Copy not allowed on types with destructors" 标签),impl_ 则是 Drop 实现的跨度,用于输出 "destructor declared here" 的 note。同文件中还定义了近亲错误 E0206CopyImplOnNonAdt,"type is not a structure or enumeration"),对应"对非 ADT 类型实现 Copy"的情形,可见 rustc 把 Copy 的合法性拆成了多条独立的检查。

3.2 判定函数:type_allowed_to_implement_copy

真正的判定逻辑位于 traits/misc.rs 中的 type_allowed_to_implement_copy。该函数按顺序做三类检查,返回不同的 CopyImplementationError 变体:

pub fn type_allowed_to_implement_copy<'tcx>(
    tcx: TyCtxt<'tcx>,
    param_env: ty::ParamEnv<'tcx>,
    self_type: Ty<'tcx>,
    parent_cause: ObligationCause<'tcx>,
    impl_safety: hir::Safety,
) -> Result<(), CopyImplementationError<'tcx>> {
    let (adt, args) = match self_type.kind() {
        // 整数、浮点、bool、char、裸指针、!、共享引用、数组
        // 这些类型历史上是 builtin impl,现在由 libcore 提供
        ty::Uint(_) | ty::Int(_) | ty::Bool | ty::Float(_)
        | ty::Char | ty::RawPtr(..) | ty::Never
        | ty::Ref(_, _, hir::Mutability::Not) | ty::Array(..) => return Ok(()),

        &ty::Adt(adt, args) => (adt, args),

        _ => return Err(CopyImplementationError::NotAnAdt),
    };

    all_fields_implement_trait(tcx, param_env, self_type, adt, args, parent_cause, LangItem::Copy)
        .map_err(CopyImplementationError::InfringingFields)?;

    if let Some(did) = adt.destructor(tcx).map(|dtor| dtor.did) {
        return Err(CopyImplementationError::HasDestructor(did));
    }

    if impl_safety.is_safe() && self_type.has_unsafe_fields() {
        return Err(CopyImplementationError::HasUnsafeFields);
    }

    Ok(())
}

检查顺序与 E0184 的关系:

  1. 类型形状检查:标量、裸指针、!、共享引用、数组直接放行(这些类型的 Copy 实现现在由 libcore 提供,注释中也说明了这一历史变迁);非 ADT 类型返回 NotAnAdt,最终对应 E0206;
  2. 字段检查all_fields_implement_trait 要求 ADT 的所有字段都满足 Copy,违反时返回 InfringingFields(附带具体违反字段的列表),用于生成逐字段的诊断;
  3. 析构函数检查(E0184 的直接来源)adt.destructor(tcx) 查询该 ADT 是否绑定了 Drop 实现,若存在则返回 HasDestructor(did) 并携带 Drop 实现的 DefId——这正是报错 note 中 "destructor declared here" 位置的来源;
  4. unsafe 字段检查:对 safe impl,含 unsafe 字段的类型不能实现 Copy,返回 HasUnsafeFields(当前分支按 delayed bug 处理)。

也就是说,E0184 的检查位置排在字段检查之后:一个既含非 Copy 字段、又实现了 Drop 的类型,会先报告字段问题;而析构函数检查以 adt.destructor(tcx) 查询为准,与 Dropimpl 还是通过其他方式绑定无关。

3.3 调度入口:一致性检查中的 visit_implementation_of_copy

Copy 实现是在一致性(coherence)检查阶段被逐一访问的。入口函数 visit_implementation_of_copy 位于 coherence/builtin.rs

fn visit_implementation_of_copy(checker: &Checker<'_>) -> Result<(), ErrorGuaranteed> {
    // ...
    let cause = traits::ObligationCause::misc(DUMMY_SP, impl_did);
    match type_allowed_to_implement_copy(tcx, param_env, self_type, cause, impl_header.safety) {
        Ok(()) => Ok(()),
        Err(CopyImplementationError::InfringingFields(fields)) => { /* 逐字段诊断 */ }
        Err(CopyImplementationError::NotAnAdt) => {
            Err(tcx.dcx().emit_err(diagnostics::CopyImplOnNonAdt { span }))
        }
        Err(CopyImplementationError::HasDestructor(did)) => {
            let span = tcx.hir_expect_item(impl_did).expect_impl().self_ty.span;
            let impl_ = tcx.def_span(did);
            Err(tcx.dcx().emit_err(diagnostics::CopyImplOnTypeWithDtor { span, impl_ }))
        }
        // ...
    }
}

完整调用链可以概括为:

rustc 编译 → 一致性检查(coherence)
  → visit_implementation_of_copy(逐 impl 访问 Copy 实现)
      → type_allowed_to_implement_copy(字段 / 析构 / unsafe 字段三级检查)
          → HasDestructor(did)
              → emit CopyImplOnTypeWithDtor(即 E0184)

其中 HasDestructor 分支里,span 取自 Copy impl 的 self_ty.span(主诊断位置),impl_ 取自 did 对应的 Drop 实现定义跨度(note 位置),与 3.1 节诊断结构体的两个字段一一对应。

四、遇到 E0184 时的处理方式

理解了检查逻辑后,修法就很直接——必须打破"Copy + Drop 共存"这一组合,常见方案有:

  1. 去掉 Drop 实现:如果类型并不真正需要自定义析构(例如资源由内部字段自行管理、或 Drop 只是误加的桩代码),直接删除 impl Drop,保留 Copy。这适用于字段全部可安全位拷贝的场景。
  2. 去掉 Copy,只保留 Drop(更常见):绝大多数需要 Drop 的类型(持有分配内存、文件句柄、锁、连接等)只应实现 Clone 而不是 CopyClone 可以逐字段深拷贝,源值在克隆后依然有效,与析构语义不冲突;代价是复制不再是零成本的位拷贝。
  3. 用指针间接持有需要析构的资源:例如把资源字段包进 Box<T>Box<T> 本身是 Copy 语义上不可直接位拷贝的,这同样会阻止外层 Copy;因此这种写法通常与方案 2 结合使用,核心思路是:需要析构的资源,其类型路径上不应出现 Copy

判断依据可以对照 3.2 节的检查顺序:只要 adt.destructor(tcx) 非空,HasDestructor 就必然触发,无论字段是否全部 Copy。所以"让所有字段都 Copy"并不能绕过 E0184,必须移除 Drop 侧。

五、如何验证与进一步阅读

  • 最小复现:将 E0184.rs 的内容保存为独立文件,用 rustc 直接编译即可看到 2.2 节所述的报错;对照 E0184.stderr 可以确认诊断的完整形态(主错误 + destructor note)。
  • 错误码文档E0184.mdrustc --explain E0184 输出内容的来源(仓库中错误码文档集中存放在 rustc_error_codes 目录)。
  • 实现链路:诊断结构 diagnostics.rs、判定函数 traits/misc.rs、调度入口 coherence/builtin.rs,三者共同构成 E0184 从"实现检查"到"诊断输出"的完整路径。
  • 关联错误码:同为 Copy 合法性检查的 E0206(对非结构体/枚举类型实现 Copy)定义于同一诊断文件中,可与 E0184 对照阅读。

总结:E0184 的本质是 rustc 在编译期强制维护"Copy 的位拷贝语义"与"Drop 的析构语义"之间不可共存的边界。文档层面它引用了历史上该组合实现不正确的安全 issue(#20126)作为禁用依据;源码层面它由 type_allowed_to_implement_copy 中的析构函数检查触发,并通过 CopyImplOnTypeWithDtor 诊断结构给出"哪里实现了 CopyDrop 声明在哪里"的精确指引。理解这一检查链后,遇到 E0184 时只需按第四节三选一即可正确修复。

登录后查看全文
热门项目推荐
相关项目推荐