【问题标题】:Why does explicit template instantiation not break ODR?为什么显式模板实例化不会破坏 ODR?
【发布时间】:2018-10-05 11:03:01
【问题描述】:

这个问题是在this answer的上下文中出现的。

正如我所料,这个翻译单元无法编译:

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 很有帮助地将我指向this relevant paragraph in the standard,但我仍然对此感到有些困惑,所以我希望有人能用更简单的术语解释这背后的逻辑(例如,应该或不应该考虑什么打破 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 的实施正是为了避免此类问题。

【问题讨论】:

  • AFAIK,除了前者的不可见性之外,隐式和显式实例化之间没有区别。
  • 是的,模板实例化是弱符号,所以链接器只选择它感觉的任何一个。不过不太确定,因此不是答案。
  • @molbdnilo 感谢您的评论。我明白你的意思,但我不相信为什么会这样。我添加了一个(复杂的)案例,其中显式初始化会产生意想不到的结果。
  • “每个程序都应包含一个定义,该定义在该程序中被丢弃的语句之外的 odr 使用的每个非内联函数或变量;不需要诊断。”
  • 强/弱符号超出了 c++ 标准。这样做是为了不强制编译器检查,IMO,因为在一般情况下检查每个 ODR 违规可能会很复杂。

标签: c++ one-definition-rule explicit-instantiation


【解决方案1】:

尤里卡!我终于落在了相关的段落上,[temp.spec]/5

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

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

  • (5.2) 一个显式特化在程序中最多只能定义一次,如 [basic.def.odr] 中所指定的,并且

  • (5.3) 除非显式实例化跟在显式特化声明之后,否则显式实例化和显式特化声明都不得出现在程序中。

诊断违反此规则的行为不需要实现。

所以显式模板实例化定义(不是隐式实例化)会导致 ODR 违规,不需要诊断(至少 gcc 和 clang - ld 工具链不会产生诊断)

【讨论】:

  • 伟大的工作,确实很清楚。我想编译器诊断这个错误比看起来更困难。顺便说一句,我认为这意味着问题中第三个示例(带有模板专业化)的编译错误实际上并没有得到标准的保证?
  • @jdehesa 在第三种情况下,您声明显式模板 specializations,这在另一段 temp.expl.spec 中处理。 (我现在才看),=> 不需要诊断
  • @jdehesa 不过好消息,如果你用 gcc 或 clang 编译,目标文件中的符号不​​弱!所以你总是会得到一个链接时间错误。
  • 哦,我明白了,谢谢,我认为您发布的段落中的 5.2 已经涵盖了这一点,并且您链接的段落是关于专业化的声明使用顺序的。无论如何,NDR无论如何!感谢您使用 gcc 和 clang 进行检查,我使用 MSVC 进行了测试,它似乎也是如此。
  • @jdehesa 实际上,最后一段的第二句与第一段的第二个项目符号是多余的。
【解决方案2】:

根据使用的上下文和生成的实体的含义,显式特化和显式实例化定义都将违反 ODR。

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

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

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

  • (5.2) 一个显式特化在程序中最多只能定义一次(根据 6.2),[...]

函数模板在定义它们的同一个翻译单元和其他翻译单元中可能有不同的实例化点,当这些特化的含义是所有实例化点都相同。

[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; }

【讨论】:

  • 感谢您的详细解释,阅读标准可以让我感觉像 Groucho Marx,但这很有帮助。你解释说第一个和第三个例子违反了 NDR 的 ODR,尽管这两个实际上是我得到编译错误的地方。你的意思是,这些编译错误并没有得到标准的保证,对吧?
  • 在 [temp.spec]/5 的同一部分中声明不需要实施诊断,这让我认为不,他们没有义务,但这些情况在 IMO 很容易识别那么,为什么不诊断它们呢?
  • Aff... 第二种情况是无效的,因为它违反了 temp.spec/5.1 它再清楚不过了。在最后一个中,ODR 违规是由于不同的模板定义参见 [basic.def.odr]/12,即使没有任何模板实例化!
  • 最后但同样重要的是,由于依赖名称查找,实例化点只能更改模板函数的定义。在模板 getNum 的任何定义中都没有依赖名称查找。因此,您的答案确实超出了主题。对不起!
  • @Oliv,对于第二种情况(5.1)涉及最多出现一次(它没有,NDR)但我不清楚的是明确声明它们构成 ODR违规,因为 [basic.def.odr]/12 指的是未指定某些模板参数(它们没有)的模板特化。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-20
相关资源
最近更新 更多