您必须将显式特化视为函数声明。就像您有两个重载函数(非模板化)一样,如果在尝试调用第二个版本之前只能找到一个声明,编译器会说它找不到所需的重载版本。与模板的不同之处在于编译器可以根据通用函数模板生成该特化。那么,为什么禁止这样做呢?因为完整的模板特化违反了 ODR,因为到那时,已经存在相同类型的模板特化。当模板被实例化(隐式或非隐式)时,也会创建相应的模板特化,这样以后使用(在同一个翻译单元中)相同的专业化将能够重用实例化并且不会为每个实例化重复模板代码。显然,ODR 也适用于模板专业化,就像它适用于其他地方一样。
因此,当引用的文本说“不需要诊断”时,它只是意味着编译器不需要为您提供有见地的评论,即问题是由于模板的实例化发生在显式专业化之前的某个时间。但是,如果它不这样做,另一种选择是给出标准的 ODR 违规错误,即“[T = int] 的 'Foo' 专业化的多个定义”或类似的东西,这不会是作为更聪明的诊断很有帮助。
对编辑的回应
1)虽然俗话说所有模板函数定义(即实现)必须在实例化点可见(这样编译器可以替换模板参数并实例化函数模板)。但是,函数模板的隐式实例化只需要函数的声明可用。因此,在您的情况下,将其分成两个翻译单元是可行的,因为它不违反 ODR(因为在该 TU 中,只有一个 的声明Foo<int>
),声明 ifFoo<int>
在隐式实例化点(通过Foo<T>
)可用,并且的定义Foo<int>
TU B 中的链接器可以使用。因此,没有人认为第二个示例“不应该工作”,它按预期工作。根据规则)。
2)在您的第一个示例中,要么会出现错误,因为编译器找不到通用函数模板(非专业实现),因此无法Foo<int>
从通用模板实例化。或者,编译器会找到通用模板的定义,使用它来实例化Foo<int>
,然后因为遇到第二个模板特化而抛出错误Foo<int>
。您似乎认为编译器会在找到您的专业之前找到它,但事实并非如此。C++ 编译器从上到下编译代码,它们不会来回替换这里和那里的东西。当编译器第一次使用 时Foo<int>
,它此时只看到通用模板,并假设该通用模板的实现可用于实例化Foo<int>
,它不期望 的专门实现Foo<int>
,它期望并将使用通用实现。然后,它看到特化并抛出错误,因为它已经确定要使用通用版本,所以它确实看到了同一函数的两个不同定义,是的,它确实违反了 ODR。就这么简单。
为什么哦为什么!!!
2 TU 案例必须工作,因为您应该能够在 TU 之间共享模板实例化,这是 C++ 的一个特性,也是一个有用的特性(如果您有少量可能的实例化,您可以预编译它们)。
不能允许 1 TU 的情况,因为在 C++ 中声明某些东西会告诉编译器“在某处定义了这个东西”。你告诉编译器“某处有模板的通用定义”,然后说“我想使用通用定义来制作函数Foo<int>
”,最后,你说“无论何时Foo<int>
调用,它都应该使用这个特殊定义”。这是一个彻头彻尾的矛盾!这就是为什么ODR 存在并适用于这种情况,以禁止此类矛盾。通用定义“to-be-found”是否不存在并不重要,编译器期望它,它必须假设它确实存在并且它与专业化不同。它不能继续说“好的,所以,我会在代码中的其他地方寻找通用定义,如果找不到,那么我会回来并‘批准’这个专业化来代替通用定义定义,但如果我找到它,我会将此专业化标记为错误”。对于使用稍后出现的特化的代码,它也不能继续通过更改明确显示使用通用模板的意图的代码(因为尚未声明特化)来完全忽略程序员的愿望。我可以'
2 TU 的情况完全不同。当编译器在编译 TU A(使用Foo<int>
)时,它会寻找通用定义,找不到它,假设它稍后会被链接为Foo<int>
,并留下一个符号占位符。然后,由于链接器不会查找模板(实际上模板不可导出),它会查找实现 的函数Foo<int>
,并且它不关心它是否是专用版本。链接器只要找到要链接的相同符号就很高兴。之所以如此,是因为对于程序员(无法轻松更改其编译库中的函数)和编译器供应商(必须实现这个链接疯狂的计划)。