为什么显式模板实例化不会破坏 ODR?

Why does explicit template instantiation not break ODR?

本文关键字:ODR 实例化 为什么      更新时间:2023-10-16

这个问题就是在这个答案的背景下出现的。

正如我所料,这个翻译单元不会编译:

template <int Num> int getNum() { return Num; }
template int getNum<0>();
template int getNum<0>();  // error: duplicate explicit instantiation of 'getNum<0>'
int main() { getNum<0>(); return 0; }

我理解这一点,我尝试两次使用相同的显式模板实例化。然而,事实证明,将其分成不同的单元,它编译:

// decl.h
template <int Num> int getNum() { return Num; }
// a.cc
#include <decl.h>
template int getNum<0>();
// b.cc
#include <decl.h>
template int getNum<0>();
int main() { getNum<0>(); return 0; }

我没想到会这样。我假设具有相同参数的多个显式模板实例化会破坏 ODR,但事实似乎并非如此。但是,这确实失败了:

// decl.h
template <int Num> int getNum();
// a.cc
#include "decl.h"
template <> int getNum<0>() { return 0; }
// b.cc
#include "decl.h"
template <> int getNum<0>() { return 0; }
int main() { getNum<0>(); return 0; }

用户 Oliv 帮助我指出了标准中的这一相关段落,但我仍然对此感到有些困惑,所以我希望有人能用更简单的术语解释这背后的逻辑(例如,应该或不应该考虑什么来破坏 ODR 以及为什么我的期望是错误的(。

编辑:

再举一个例子,这里有一个程序分为两个单元,编译正确,但它产生了可以说是令人惊讶的结果:

// a.cc
template <int Num> int getNum() { return Num + 1; }
template int getNum<0>();
// b.cc
#include <iostream>
template <int Num> int getNum() { return Num; }
template int getNum<0>();
int main() { std::cout << getNum<0>() << std::endl; return 0; }

输出:

1

在这种情况下,删除显式模板实例化会产生0。我知道拥有两个具有不同定义的模板不是一个常见的用例,但我认为 ODR 正是为了避免此类问题而强制执行的。

尤里卡!我最后落在相关段落上,[temp.spec]/5

对于给定的模板和一组给定的模板参数,

  • (5.1( 显式实例化定义在程序中最多应出现一次,

  • (5.2( 明确的专业化应在程序中最多定义一次,如 [Basic.def.odr] 中所述,并且

  • (5.3( 显式实例化和显式专用化声明均不得出现在程序中,除非显式实例化遵循显式专用化声明。

不需要实现来诊断与此规则的冲突。

因此,显式模板实例化定义(非隐式实例化(可能会导致 ODR 违规,无需诊断(至少 gcc 和 clang - ld 工具链不会产生诊断(

显式专用化和显式实例化定义都将违反 ODR,具体取决于它们使用的上下文和它们生成的实体的含义。

下面解释第一种和第三种情况,以及为什么它们确实违反了 ODR 和 NDR[temp.spec]/5

对于给定的模板和一组给定的模板参数,

  • (5.1( 显式实例化定义在程序中最多出现一次,

  • (
  • 5.2(在一个计划中最多应定义一次明确的专业化(根据6.2(,[...]

函数模板在定义它们的同一翻译单元和其他翻译单元中可能具有不同的实例化点,当这些专用化的含义在所有实例化点中都相同时,可以保证这些专用化不会违反 ODR。

[temp.point]/6

显式实例化

定义是由显式实例化指定的一个或多个专用化的实例化点。

[temp.point]/8

[...]如果两个不同的实例化点根据一个定义规则 (6.2( 赋予模板专用化不同的含义,则程序格式不正确,无需诊断。

第二种情况不违反 ODR,因为这些 TU 中实例化的含义是相同的。

// decl.h
template <int Num> int getNum() { return Num; }
// a.cc
#include <decl.h>
template int getNum<0>();
// b.cc
#include <decl.h>
template int getNum<0>();
int main() { getNum<0>(); return 0; }

但最后一个肯定不是有效的(违反 ODR NDR(,因为即使函数模板具有相同的签名,它们的实例化也会有不同的含义。你不能传递你得到的结果,标准不保证这些违规发生时的行为。

// a.cc
template <int Num> int getNum() { return Num + 1; }
template int getNum<0>();
// b.cc
#include <iostream>
template <int Num> int getNum() { return Num; }
template int getNum<0>();
int main() { std::cout << getNum<0>() << std::endl; return 0; }