news 2026/9/11 21:35:13

Carbon 语言前向声明的合并规则与 `extern` 关键字设计全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Carbon 语言前向声明的合并规则与 `extern` 关键字设计全解析

Carbon 语言前向声明的合并规则与extern关键字设计全解析

【免费下载链接】carbon-langCarbon Language's main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang

本篇技术指南围绕 Carbon Language 仓库中的设计提案 p003762-merging-forward-declarations 展开,系统讲解 Carbon 中前向声明(forward declaration)与定义(definition)如何合并、extern关键字在跨库声明中的语义与约束、修饰符关键字在声明与定义之间的匹配规则,以及这些设计如何在编译器中落地。读完本文,你将掌握 Carbon 前向声明的完整规则矩阵,理解extern声明在库边界上的作用,并能依据提案与设计文档判断一段 Carbon 声明代码是否合法。

提案背景:前向声明的合并存在哪些歧义

在 Carbon 中,前向声明可以与定义合并,前提是两者"匹配"(match)。然而在提案提出之前,设计上存在三处明显的语义模糊:

  • 前向声明能否重复:同一实体是否可以重复前向声明,包括在定义之后再次出现;
  • 是否需要关键字区分:是否需要引入一个关键字,将"只是声明、由其他库提供定义"的声明与"本库即将给出定义"的声明区分开;
  • 修饰符是否必须一致:前向声明与定义上的修饰符关键字(modifier keywords)是否需要保持匹配。

前向声明在当前设计中的位置

提案回顾了当时设计文档中对前向声明的既有规则,包括:

  • 顶层语言设计中的声明、定义与作用域规则,见 docs/design/README.md#declarations-definitions-and-scopes;
  • 类的前向声明规则,见 docs/design/classes.md#forward-declaration(类前向声明以分号;结尾,声明的类型被视为不完整类型,直到对应定义出现);
  • 函数的前向声明规则,见 docs/design/functions.md;
  • 泛型体系中implinterface的前向声明、以及"匹配与一致"(Matching and agreeing)的通用规则。

ODR(单一定义规则)的启发

C++ 的 ODR 要求每个实体有且只有一个定义,但 C++ 在编译期很难检测 ODR 违规,链接期检测也并不可靠。Carbon 同样遵循"一个实体只能被一个库定义"的原则:其他库可以前向声明该实体,但这种声明不构成定义。提案明确指出,引入extern声明语法的部分目标,就是基于声明更好地诊断 ODR 违规

匹配声明的合并前提

当两个声明同名(无论在同一文件内,还是经由导入到达)时,它们必须"匹配",否则会被诊断为冲突。例如:

  • fn F(); fn F() {}—— 可以合并,因为函数签名匹配、前向声明合法、且只有一个定义;
  • fn F(); fn F(x: i32);—— 不能合并,因为参数列表不同,会触发诊断。

提案总览:三条核心改动

提案给出三项核心结论:

  1. 新增extern声明
    • extern用于标记"实体由不同库定义"的前向声明;
    • 定义实体的库必须在api文件中将该实体声明为 public;
    • 定义库本身将实体声明为extern是非法的;
    • extern只能用于不包含该实体定义的库中的声明;
    • 在修饰符序列中,extern紧跟访问控制关键字之后、其他关键字之前;
    • 实体在一个文件中被声明为extern,则整个文件内都必须以extern方式使用它;且在该extern声明之前,实体完全不可被使用(即使存在导入的声明)——这保证了增删声明时的重构安全;
    • 若一个库在apiimpl中以非extern方式声明了某实体,则该库内其他地方将其声明为extern均为非法。
  2. 前向声明只能出现一次,且必须在定义之前:同一实体在给定文件内至多前向声明一次,前向声明只允许出现在定义之前。
  3. 修饰符关键字的合并规则按实体类型逐个裁定(详见下文"修饰符关键字"一节)。

extern关键字详解

extern是一个新的修饰符关键字,加在前向声明上,表示"该实体不由声明它的库定义"。使用extern声明实体的库不能定义该实体;只有定义实体的库可以省略extern,而省略extern就意味着必须提供定义。

extern声明形成的实体没有定义、也没有存储。例如:

  • extern fn—— 形成一个可以被调用的函数;
  • extern class—— 形成一个不完整类型(incomplete type);
  • extern interface—— 形成一个未定义的接口;
  • extern varextern let—— 绑定名字但不分配存储,禁止初始化器

extern关键字不能用在仅有声明语法、没有存储的实体上(如aliasnamespace);同时它只对命名空间作用域(namespace-scoped)的名字合法,在类型作用域内非法(例如class Foo { extern fn Member(); }是不允许的)。

在声明修饰符中,extern紧跟访问控制关键字之后、其他关键字之前,例如private extern <other modifiers> class B;。在当前设计下,当extern出现在声明上时,只有访问修饰符与之合法搭配(详见"修饰符关键字"一节)。

编译器中的extern关键字

extern在 Carbon 工具链的词法层有明确登记:见 toolchain/lex/token_kind.def 中的CARBON_KEYWORD_TOKEN(Extern, "extern")。在语义检查阶段,编译器多处维护实体的is_extern标记,例如 toolchain/check/cpp/import.cpp、toolchain/check/function.cpp、toolchain/check/global_init.cpp 等位置,均会以is_extern = false构建实体信息;声明名栈(toolchain/check/decl_name_stack.h)在处理声明时也会接收is_extern参数。这印证了extern是贯穿词法、语法与语义分析的正式语言特性,而非纯文档约定。

声明何时被允许:五条规则

判断一个声明是否被允许,需要同时满足以下三条基本原则,再辅以若干细化规则:

  1. 声明应当总是新增信息——定义之后不允许再出现声明;
  2. 一个实体只能由一个库以非extern方式声明
  3. 支持在不影响客户端库编译的前提下,将声明在已被导入的api文件之间移动。

规则一:前向声明不得跟在声明之后

在一个文件中,前向声明绝不能跟在同一实体的前向声明或定义之后。例如:

class A { ... } // Invalid: Disallowed after the definition. class A; class B; // Invalid: Disallowed due to repetition. class B; class C; // Valid: Allowed because the definition is added. class C { ... }

规则二:文件必须要么使用导入的声明,要么声明自己的

如果一个实体的声明或定义被导入到某文件,该文件必须二选一:要么使用导入的版本,要么声明自己的版本,不能两者兼用。示例如下:

package Foo library "a" api; class C { ... }
package Foo library "b" api; import library "a"; extern class C; // Valid: Uses the incomplete type of the extern declaration. fn Foo(c: C*);
package Foo library "c" api; import library "a"; // Valid: Uses the complete type of the imported definition. fn Foo(c: C);
package Foo library "d" api; import library "a"; fn Foo(c: C); // Invalid: `Foo` used the imported `C`, so an `extern` declaration of `C` is now // invalid. extern class C;

规则三:库不能既定义实体又将其声明为extern

如果某个库的impl定义了实体,则其api不得以extern方式声明它:

package Wiz library "a" api; extern class C;
package Wiz library "a" impl; // Invalid: The `api` file declared `C` as `extern`. class C { ... }

反过来,api中可以存在extern声明,而impl导入并使用该实体的定义——因为impl文件并没有声明该实体,所以二者并不冲突。

规则四:impl文件中的前向声明必须包含定义

impl文件中,如果前向声明了某实体,则必须同时在该文件提供定义。对于拥有多个impl文件的库,这意味着:在一个impl文件中使用定义于另一个impl文件的实体,需要在api文件中提供(可能是private的)前向声明。此处不能使用extern声明,因为该库本身定义了实体。

这条规则让 Carbon 能够在编译期诊断"impl中声明了实体却没有本地定义"的问题。注意:api中声明的实体,除非编译器被告知已看到全部impl文件,否则可能无法得到编译期诊断。

package Bar library "a" api; class C; class D { ... }
package Bar library "a" impl; // Invalid: Missing a definition in the `impl` file, but if one were added, then // this would be valid. class C; // Invalid: The `api` defines `D`. As a consequence, there is no way to make // this forward declaration valid. class D;

这条规则并不禁止impl提供前向声明——当它同时提供定义时仍然允许,这可用于解开依赖环。例如api中声明class D;impl中通过本地前向声明配合定义来打破两个类之间的循环依赖:

package Bar library "a" api; class D;
package Bar library "a" impl; class D; class E { fn Fself: Self { ... } } class D { fn Gself: Self { ... } }

这里impl不能直接使用api中的导入前向声明(受"规则二"限制),因此若没有class D;F的定义将不合法。

规则五:类型作用域内可以同时存在前向声明与定义

前向声明与定义的组合在类型作用域内是允许的

class C { class D; fn F() -> D; class D { fn G() -> Self { return C.F(); } var x: i32; } fn F() -> D { return {.x = 42}; } }

这是必要的,因为类型体(type body)不会像函数体那样被自动移到类外处理。

extern声明与extern impl的交互

externimpl声明不得在其类型结构中使用extern类型。考虑两个库:一个定义A并将B声明为extern,另一个定义B并将A声明为extern。这种情况下,两个库都不应能定义同时涉及ABimpl,否则两个库都可能给出定义,造成冲突。

涉及extern类型的impl查找

当涉及extern类型的impl查找命中一个非final的参数化impl时,查找成功,但该接口关联实体(associated entities)的取值全部未知。原因是可能存在另一个更特化的impl恰好适用,但当前不可见(这与受约束泛型中发生的情况一致)。

library "a" api; extern class C; extern class D(T:! type); extern impl forall [T:! type] D(T) as I where .Result = i32;

在上例中,D(C)实现了I,但.Result未知——因为它可能不是i32

修饰符关键字的合并规则

提案对前向声明与定义之间的各类修饰符给出了明确裁定:

  • extern:只对前向声明合法,规则如前所述。
  • extendextend impl):只出现在类体中的声明上(无论是前向声明还是定义)。
  • 类、impl、接口修饰符abstractbasefinal):只存在于定义上,不在前向声明上。例如base加在class C { ... }上是合法的,但加在class C;上被禁止。
  • 函数修饰符implvirtualdefaultabstractfinal):必须在前向声明与定义之间完全匹配。这只影响类型作用域的名字(因为它们在命名空间名字上非法);abstract因为没有定义,在此无实际影响。
  • 访问修饰符privateprotected):必须匹配,但存在一个例外——extern名字在真实名字为 public 时可以被声明为private
    • 这允许一个库前向声明另一个库的类型,而不让客户端依赖它的前向声明;
    • 合并时,更公开的声明优先,隐藏private extern声明。
    • 该规则同时影响类型作用域与命名空间作用域的名字。

一个值得注意的统一模式是:定义上的修饰符通常是声明上修饰符的超集extern是例外)。看声明永远只能得到不完整视图,而看定义可以得到完整视图。

设计动机:提案与项目目标的对齐

提案在 Rationale 中将其设计决策与 Carbon 项目目标逐一对应:

  • 软件与语言演进(docs/project/goals.md#software-and-language-evolution):支持在不影响客户端编译的前提下把类在不同库之间移动。允许在导入非extern声明的同时保留冗余extern声明,从而支持"向已导入库中添加类";要求使用本地extern声明,则防止了可能阻碍"从导入库中移除类"的意外使用。只要求extern声明携带影响调用约定的关键字,意味着大多数情况下可以在定义库中增删关键字而不破坏其他库的extern声明。
  • 易于阅读、理解与编写:显式标注extern帮助读者识别"库为了规避依赖环而使用外部类型"的场景;禁止冗余前向声明消除了混淆来源;类型作用域函数要求重复关键字则提升了可读性。
  • 务实的安全与测试机制:要求extern声明被清晰标记,提升诊断 ODR 违规的能力,帮助开发者发现微妙的正确性问题。
  • 快速可扩展的开发extern声明被视为支持库独立编译(separate compilation)的关键,进而支撑编译规模的扩展。
  • 与现有 C++ 代码的互操作与迁移extern的选择与 C++ 保持一致,语义相近但略有不同。
  • "倾向于只提供一种做事方式"原则(docs/project/principles/):为"关键字该放在前向声明还是定义上"设定明确要求,而非让位置可选用,使得开发者可以通过"看到或没看到"哪些关键字来做出判断。

备选方案分析:为什么这样设计

提案详细记录了多个被否决或搁置的备选方案,理解这些权衡有助于把握规则背后的意图。

其他修饰符关键字合并方案

设计团队权衡了以下因素:

  • 修饰符出现位置的一致性很有价值。若给一个有独立定义的实体添加关键字,可能需要同时修改前向声明与定义;不允许添加在不必要的位置。例如base只能加在定义上。虽然可以让关键字在"不影响语义"的前提下可选,但设计者更希望"关键字的有无"承载明确含义——若允许base class C;,相邻的class D;很容易被误读为"D 不是 base 类",而实际上它只表示"D 可能是也可能不是 base 类"。

  • 访问控制需要一定的一致性,以保证前向声明的使用者重构后仍可使用定义。若访问控制超出 public 与private的范围,extern规则需要更细化的设计(例如当前语言尚没有的 package-private 类型不应能通过extern声明被公开)。

  • 类型而言,声明上使用最小化修饰符:被禁止的abstractbasefinaldefault对使用没有影响(前向声明的类型不完整),强制要求它们只会泄露实现细节并制造额外负担,也与 C++ 不太一致。extern是特例,因为它的存在本身是语义的内在部分。

  • 类型作用域成员而言,选择在声明与定义之间重复修饰符,例如:

    class A { private class B; } // `private` is required here. private class A.B { ... }

    这更强调"函数声明可复制粘贴"的价值(所有参数也必须一起复制);C++ 中static仅出现在声明上的做法被视为摩擦而非收益。virtual这类影响调用约定的修饰符必须出现在首个声明上。代价是类名被插入到类外定义的中段,而非靠近开头。

另一种被讨论的路径是:对类型成员函数,在类外定义跟随前向声明时禁止大多数修饰符——这能最大化复制粘贴能力(理想情况下fn之前的内容全部可省略),但会损害定义的可理解性。最终提案选择更冗长但更清晰的方案。

还有一种思路是允许灵活选择关键字放置位置(如函数定义上的关键字必须是声明上关键字的子集),但这会损害可读性:同一文件中的两个函数声明相似,定义却带不同关键字,会暗示并不存在的行为差异。出于"只提供一种方式"原则,提案选择规定性更强的做法。

不要extern关键字

如果没有extern,声明将不提供任何"该库是否包含定义"的线索。面对不同api文件中的两个前向声明,要么都有定义、要么都没有定义,有时只能在链接期(且依赖一些技巧)才能发现。加入extern后,可以在编译期就评估更多案例并给出警告,同时给读者提供"实体是否预期在本库稍后声明"的提示。

提案用一个例子说明:库 "b" 的api导入库 "a" 并写下class C {},而库 "a" 的api只有class C;。没有extern时这段代码预期能编译,问题留给链接器;加入extern后,处理库 "b" 时就能发现与库 "a" 的class C;前向声明之间的冲突。

结论是:extern关键字主要是为了诊断与可读性而引入,它并不从根本上改变可诊断问题的总量,而是让一部分原本属于链接期诊断或完全漏检的问题提前到编译期。

更宽松的声明限制

提案选择对声明施加严格限制,首要目的是避免诸如"定义之后再写一个无效声明"这类令人困惑的代码:

class C { ... } // This declaration has no effect. class C;

但宽松方案还允许这样的代码:

package Foo library "a" api; class C { ... }
package Foo library "b" api; import library "a"; fn F(c: C) { ... } extern class C; fn G(c: C*) { ... }

这里的歧义在于:G看到的是extern带来的不完整类型,还是被导入的完整定义?导入库 "b" 的第三方看到的C又是不完整还是完整?为消除这些可理解性问题,提案选择了更严格、同时禁止两种写法的方案。

extern的命名

extern外还考虑过externalextern在 C++ 中隐含外部链接(external linkage)语义,选它主要是为了与 C++ 保持有限的一致性。

extern默认私有

曾考虑让extern自带相当于private的访问控制含义,并引入export关键字来显式导出符号(也可能提供export import语法以再导出导入库的全部符号)。提案从三点论证了否决理由:

  1. 语义一致性extern声明若与其他声明访问控制不同,会形成另一套可见性模型,可能与"external"含义不符直觉;export与 C++ 的export关键字呼应,可能让开发者误以为其他语义也同于 C++。

  2. 与更多访问控制特性的交互:未来可能引入比 public/private 更细粒度的访问控制(如类似 Javapackage的 package-private)。表格对比了三种默认可见性下的写法:

    默认可见性Public(提案方案)Private(备选方案)
    Publicextern class C;export extern class C;
    库私有private extern class C;extern class C;
    包私有(假设)package extern class C;export package extern class C;

    提案还以 gtest 的 internal header 目录为例说明 package-private 可见性的潜在用途。

  3. public extern 的风险extern fn可被调用,存在"内部使用的extern函数被意外依赖"的风险,可通过要求extern的访问控制不宽于原符号来缓解(两者同时可见时可校验)。extern class是不完整类型,不能被实例化,但可声明指针和引用;若extern class默认私有并不能阻止其使用(如auto捕获返回指针)。

综合权衡后,提案采用常规访问控制语义,并保留后续根据实际语义演进重新审视的可能性。

不透明类型(Opaque types)

省略extern意味着库中必须有定义。若将来需要"由库拥有、没有定义"的不透明类型,可能的解法是增加表示不透明类型的修饰符关键字。就目前而言,在库的impl文件中给出空定义可达到类似效果:

package Foo library "a" api; // An opaque type which can be imported by other libraries. class C;
package Foo library "a" impl; // An empty definition. This could be in its own file, or at the end after logic, // to prevent misuse. class C {}

api的使用者无法提供定义。提案当前要求定义必须存在,这一选择可能根据使用场景重新审视。

要求库提供自己的extern声明

曾有人提议限制:库必须通过单独的extern文件(类似api/impl)或在api文件中附加标记(自动生成extern子集)来提供extern声明。其优点包括:集中管理extern声明所有权;简化重构(当前计划要求extern声明与定义库的声明保持一致,包括参数名等细节,改动一处就需要原子地改动所有声明,而限制作用域能减轻这种原子重构负担);降低 C++ 前向声明迁移的复杂度。

缺点则包括:使希望使用extern声明的库难以对单个声明做最小化导入(extern文件的导入集将是这些声明的导入集的超集);不支持 C++ 中可能出现的使用场景,如class C { ... }; extern fn F(c: C);(同一文件中extern依赖已定义实体)——虽然可以通过重构(移动实体到其他库、让extern声明只用extern声明、或用泛型编程制造间接层)解决,但这些重构可能阻碍采用。

目前评估认为缺点大于优点;如果未来积累更多实际用法,可能重新审视,但在"只提供一种方式"原则下,若非带来实质价值,应谨慎提供第二种extern语法。

允许跨包extern声明

C++ 没有包边界,跨包extern在 C++ 中实际存在,放弃支持会制造迁移障碍。但设计团队强烈倾向于限制跨包声明,以减少重构成本:跨包声明要求特定名字不能改变其声明类别(class必须保持class,不能变成alias),参数(函数或泛型)必须保持不变,甚至不允许隐式转换。包边界在平衡重构成本方面发挥着重要作用

附录:C++ 前向声明的迁移路径

提案附录并未确定具体迁移方案,但给出了迁移时的思考框架。预期迁移一个前向声明需要先识别定义该实体的 Carbon 库,然后:

  • 依据库与包边界调整前向声明:
    • 若在 Carbon 中该前向声明被禁止,可能需要删除;
    • 若前向声明与定义库相同,无需extern
    • 若在不同库但同一包内,添加extern
    • 若在不同包内,前向声明必须删除,取而代之有两种按启发式选择的方案:其一,添加对真实定义的依赖(当定义库依赖复杂时可能不可行);其二,在另一包中添加提供必要extern声明的库(当该包不属于被迁移方时可能不可行)。
  • 若前向声明的代码在 C++ 中,需要保留 C++ 侧的前向声明,可能的做法包括:为 C++ 代码提供文件内前向声明的 Carbon 语法(如extern cpp <declaration>);或创建提供前向声明的小型 C++ 头文件并依赖它。
  • 修正参数名的有意义的差异(例如将前向声明的参数名更新为与定义一致)。
  • 修正修饰符关键字的差异(例如将函数前向声明的修饰符补充到定义上)。

后续演进:从提案到当前设计

需要特别说明的是,p003762-merging-forward-declarations 是 Carbon 前向声明设计历程中的里程碑提案,其后的设计在此基础上继续演进:

  • 提案 p003763-matching-redeclarations 进一步细化了声明匹配(syntactic matching)规则;
  • 提案 p003980-singular-extern-declarations 实质上重写了extern特性(其 "Versus proposal #3762" 一节明确写道"extern特性的任何部分都不应假定仍然适用"):实体最多可有三个声明——可选的非持有方extern library "<owning_library>"声明(必须位于定义库之外,且定义库的api文件必须导入它并同时包含一个声明)、可选的持有方前向声明(必须先于定义,api文件视为位于impl文件之前)、以及必需的持有方定义;并统一了extern与非extern类型的一致性(type coherency)。
  • 当前设计文档 docs/design/declaring_entities.md 汇总了两者的成果:extern在持有方声明上限制对定义的访问(必须显式导入才能获得完整类型),在非持有方声明上以extern library "<owning_library>"形式允许在不依赖定义库的前提下引用实体;extern仍只对命名空间作用域实体合法(class C { extern fn F(); }非法);对间接导入,extern实体的定义不可见,直接导入定义即可解决。该文档的 "Alternatives considered" 一节直接链接回本提案的各个备选方案小节,可见 p003762 的权衡分析仍是后续设计的重要参考。

对于希望跟踪实现现状的读者,建议同时阅读 docs/design/functions.md(函数前向声明与extern fn的使用)、docs/design/classes.md#forward-declaration(类前向声明)、docs/design/README.md(声明、定义与作用域总览)以及 toolchain/lex/token_kind.def(extern词法关键字登记),从而从设计文档到工具链实现形成完整闭环。

【免费下载链接】carbon-langCarbon Language's main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 21:35:00

Jenkins CI/CD自动化部署实战指南

1. Jenkins与CI/CD核心概念解析Jenkins作为开源的自动化服务器&#xff0c;已经成为现代软件工程中不可或缺的基础设施。我第一次接触Jenkins是在2013年一个电商系统的重构项目中&#xff0c;当时团队正苦于手动部署导致的频繁人为错误。引入Jenkins后&#xff0c;部署错误率从…

作者头像 李华
网站建设 2026/9/11 21:34:26

Java NIO文件处理性能陷阱与优化实践

1. 项目概述&#xff1a;NIO文件处理的真相与陷阱第一次用Java NIO的FileChannel复制文件时&#xff0c;我盯着任务管理器里飙高的CPU使用率愣住了——这和传说中的"高性能NIO"相去甚远。经过反复测试验证&#xff0c;终于揪出了这个藏在API文档角落的性能陷阱&#…

作者头像 李华
网站建设 2026/9/11 21:32:44

跨境电商运营和国内电商运营有什么区别?2026 差异化打法解析

摘要&#xff1a;跨境电商运营和国内电商运营表面像姐妹&#xff0c;实则链路和逻辑差得很远。本文从数据获取、合规成本、库存周转三个维度拆解两者的本质差异&#xff0c;并给出跨境场景下的选型建议&#xff0c;帮想从国内转向跨境的卖家少踩几个坑。 国内电商如同熟悉的高…

作者头像 李华