如何严格定义功能模板显式实例化规则?

Pre*_*nik 5 c++ language-lawyer function-templates explicit-instantiation

注意:这个问题是关于显式实例化的,而不是显式专业化的。

请看下面的例子:

template <bool A, typename X>
void f (X &x) {} // 1
template <bool A>
void f (int &x) {} // 2
template void f<true> (int &x); // 3
Run Code Online (Sandbox Code Playgroud)

假设我的最初目标是仅显式实例化第二个功能模板,A = true因此我编写line // 3。但是直观上,第一个定义也可以用line显式实例化,// 3这有点麻烦,因为我无法用当前的语法实际转义它,因为bool A在我的情况下无法推断出它。从理论上讲,我什至都不介意两个函数模板最终都被显式实例化,但是最有趣的部分是实际的编译结果。

(在编译成功的所有情况下,仅实例化第二个功能模板。)

  1. 原案。用msvc和编译clang。无法使用以下命令进行编译gcc

    错误:“ void f(int&)”的模版模板特殊性“ f”

  2. 在第一个功能模板中替换bool Abool A = truegcc即可对其进行编译。

  3. 替换X &xX &&x(转发参考)会使clang无法使用以下命令进行编译:

    错误:“ f”的显式实例化的部分排序不明确

这是最激烈的案例的演示

(使用了godbolt上可用的编译器的最新版本)

所以我的问题是-在这种情况下,显式实例化行为的定义是否真的那么弱,以至于很容易进入此类雷区,或者msvc是否最符合标准?就我个人而言,即使与当前语法有些冲突,我也不认为我的最初目标是超凡脱俗。

Bri*_*ian 3

基本上,显式实例化指令是根据以下 3 个步骤过程进行解释的:

\n
    \n
  1. 对显式实例化的名称执行名称查找以确定“候选”模板的列表。
  2. \n
  3. 执行模板参数推导以确定这些模板是否“可行”。
  4. \n
  5. 如果有多个“可行”模板(这只能发生在函数模板的情况下,因为其他类型的模板无法重载),但其中一个比所有其他模板更专业,则显式实例化指令显式实例化那个模板。如果不是,则该程序格式错误。
  6. \n
\n

我从重载解析术语中借用了术语“候选”和“可行”。它们用引号引起来是为了提醒您我正在以标准没有的方式使用它们。

\n

我们现在将回顾其中每一个的语言措辞。

\n

第 1 步与您的问题无关,因为显然fin// 3// 1// 2作为候选者。但 C++23 草案中有关于此的明确规则(我不认为它们在 C++17 或 C++20 中),[dcl.meaning.general]/3:

\n
\n

否则:

\n
    \n
  • 如果声明符declarator-id中的id-expression限定 ID Q,则令S为其查找上下文(6.5.5);声明应位于命名空间范围内。
  • \n
  • 否则,令S为与声明符所占据的范围关联的实体。
  • \n
  • 如果声明符声明显式实例化或部分或显式特化,则声明符不绑定名称。如果它声明了一个类成员,则不查找声明者 ID的终端名称;\n 否则,在识别声明的任何函数模板特化时,仅考虑S中可命名的那些查找结果 (13.10.3.7)。
  • \n
\n

[...]

\n
\n

在您的情况下,declarator-id in// 3是不合格的,因此S是这些声明所在的名称空间(例如,如果您的代码表示完整的翻译单元,则S是全局名称空间),并且“候选者”是不合格的结果在Sf中可提名的名称查找。根据 [basic.scope.scope]/6,只有那些具有S目标范围或属于S内联命名空间集的命名空间的声明才是可提名的 [1]。由于和的目标范围为S,因此它们是候选者。// 1// 2

\n

步骤 2 和 3 由 C++17 [temp.deduct.decl] 描述(我指的是 C++17,因为这是发布此问题时的当前标准):

\n
\n

在其declarator-id引用函数模板的特化的声明中,将执行模板参数推导来标识该声明所引用的特化。具体来说,这是针对显式实例化 (17.7.2)、显式特化 (17.7.3) 和某些友元声明 (17.5.4) 完成的。这样做还可以确定释放函数模板特化是否与布局匹配\n operator new(6.7.4.2、8.3.4)。在所有这些情况下,P函数模板的类型是否被视为潜在匹配,并且A是声明中的函数类型或与放置匹配的释放\n函数的类型operator new 。扣除按 17.8.2.5 中所述进行。

\n

如果对于这样考虑的一组函数模板,在考虑部分排序后没有匹配或有多个匹配(17.5.6.2),则推导失败,并且在声明情况下,程序是\nill-formed的。

\n
\n

我将跳过类型推导的细节,因为很明显,它对 和 都成功// 1// 2其中bool A是从显式指定的模板参数获得的true,在 的情况下// 1X被推导为int

\n

所以最后我们有步骤 3:部分排序,如 17.5.6.2 ([temp.func.order]) 中所述。我引用相关段落:

\n
\n

部分排序通过依次转换每个模板(请参阅下一段)并使用函数类型执行模板参数推导,选择两个函数模板中哪一个比另一个更专业。推导过程确定其中一个模板是否比另一个更专业。如果\n是这样,则部分排序过程会选择更专业的模板。

\n

为了生成转换后的模板,对于每个类型、非类型或模板模板参数(包括其模板参数包(17.5.3))分别合成唯一的类型、值或类模板,并将其替换为该参数的每次出现在模板的函数类型中。[...]

\n

使用转换后的函数 template\xe2\x80\x99s 函数类型,针对其他模板执行类型推导,如 17.8.2.4 中所述。

\n
\n

17.8.2.4 [temp.deduct.partial] 解释了如何使用推导结果来确定哪个模板更专业(p8):

\n
\n

[...] 如果给定类型的推导成功,则认为参数模板中的类型至少与参数模板中的类型一样专业。

\n
\n

p12 包含以下重要的附带条件:

\n
\n

在大多数情况下,如果并非所有模板参数都有值,则推导会失败,但出于部分排序的目的,如果模板参数未在用于部分排序的类型中使用,则模板参数可能会保持没有值。[注意:在非推导上下文中使用的模板参数被视为已使用。\xe2\x80\x94尾注]
\n[示例:

\n
template <class T> T f(int);           // 1\ntemplate <class T, class U> T f(U);    // 2\nvoid g() {\n  f<int>(1);                           // calls #1\n}\n
Run Code Online (Sandbox Code Playgroud)\n

\xe2\x80\x94结束示例]

\n
\n

在标准中给出的示例中,我们处于函数调用的上下文中,因此用于推导的类型是为其提供参数的函数参数类型(p3.1)。由于T(在两个模板中) 仅在返回类型中使用T,因此它不是用于推导的任何类型的一部分。这意味着推导过程在从推导时成功(因为可以推导为),而在其他方向推导则失败。这意味着// 2// 1Uint// 1更加专业。

\n

在 OP 的代码中,使用函数本身的类型,因为这不是函数调用上下文 (p3.1)。A无法在任一方向上推导,但它也不参与函数类型,因此无法推导的事实被忽略。显然,可以从 的类型X推导出来。这告诉我们至少与 一样专业。如果我们反过来做,合成的唯一类型将与in不匹配。所以至少不像// 1// 2// 2// 1Xint// 2// 1// 2

\n

Clang 和 MSVC 是对的。// 2,作为明确更专业的模板,被显式实例化。海湾合作委员会是错误的。Fedor(在评论中)提供了 GCC 错误报告的链接。评论 3与您的示例非常相似。

\n

[1] 这里规定的可提名性要求 \xe2\x80\x94 对于合格和不合格的声明符 ID \xe2\x80\x94 基本上只是说你必须说明你正在实例化或专门化的实体的确切范围。如果您使用非限定名称f,那么它不会显式实例化f从封闭名称空间命名的任何模板。如果您使用限定名称,例如N::f,它不会f从 的封闭命名空间显式实例化N。这就是自 C++98 以来编译器的工作方式,但我认为在 C++23 之前从未明确地写下这些规则。

\n