模板部分专用化和多个编译单元

Template partial specialization and multiple compilation units

本文关键字:编译 单元 板部 专用      更新时间:2023-10-16

我无法从此代码的不同编译器中获得一致的行为,并试图找出我可能做错了什么。 基本设置有一个模板类(打印机),一个部分专用化(用于奇数值整数),它添加了一个静态 const 成员。 当我尝试以各种不同的方式定义静态常量成员时,就会出现问题。

我已将代码减少到三个文件。 为了回答一个直接的问题,我避免使用 c++11,所以我提供了一个自定义enable_if模板。

文件"template_bug.h">

template<bool B, class T = void> struct my_enable_if          {};
template<        class T       > struct my_enable_if<true, T> { typedef T type; };
template< int Value, typename = void > struct Printer {
static void doIt() { std::cout << "Printer<" << Value << "(even)>::doIt()" << std::endl; }
};
template< int Value > struct Printer< Value, typename my_enable_if<Value & 1>::type > {
static const char *c_prefix;
static void doIt() { std::cout << "Printer<" << c_prefix << Value << "(odd)>::doIt()" << std::endl; }
};
template<> const char *Printer<1>::c_prefix = "One_";

文件"other.cc":

#include <iostream>
#include "template_bug.h"
template<> const char *Printer<5>::c_prefix = "Five_";

文件"main.cc":

#include <iostream>
#include "template_bug.h"
template<> const char *Printer<3>::c_prefix = "Three_";
int main(void)
{
Printer<1>::doIt();
Printer<3>::doIt();
Printer<5>::doIt();
return 0;
}

现在,我的理解是,这段代码应该编译并且不会调用任何未定义的行为,但我觉得我一定以某种方式错了。 当我用 g++ (4.8.4) 编译它时:

g++ -c -o main.cc.o main.cc
g++ -c -o other.cc.o other.cc
g++ -o template_bug main.cc.o other.cc.o

我在链接阶段得到这个:

other.cc.o:(.data+0x0): multiple definition of `Printer<1, void>::c_prefix'
main.cc.o:(.data+0x0): first defined here

我的印象是,这个符号应该有弱链接,特别是为了避免这个错误。

问题 1:为什么Printer<1>::c_prefix不是弱符号(在 g++ 下)?

如果我注释掉Printer<1>用法,其余的将按预期编译、链接和执行。


在不同的编译器(格林希尔斯)上,我得到的错误是:

[elxr] (error #412) unresolved symbols: 1
Printer<N1, my_enable_if<(bool)(N1&((int)1)), void>::type>::c_prefix [with N1=(int)5]     from main.o

根本原因是编译器选择在两个编译单元之间生成不同的符号名称。 使用 nm,我可以看到差异:

从 nm other.o:

00000000 D c_prefix__S__94Printer__ps__63_XZ1ZQ2_48my_enable_if__tm__28_XOcsb_1_Oad_2_Z1ZCiL_1_1OOv4type__tm__9_XCiL_1_5

从 nm main.o:

U c_prefix__94Printer__ps__63_XZ1ZQ2_48my_enable_if__tm__28_XOcsb_1_Oad_2_Z1ZCiL_1_1OOv4type__tm__9_XCiL_1_5

在与他们的支持下交换了几封电子邮件后,我被告知我的代码正在调用 UB,但他们对为什么这是 UB 的解释没有意义。

问题 2:关于Printer<5>用法是否调用未定义的行为?

问题 2:关于Printer<5>用法调用未定义行为的任何内容?

在使用之前必须声明专业化,否则程序格式不正确(无需诊断)。

所以正确的方法是:

文件"template_bug.h">

template<bool B, class T = void> struct my_enable_if          {};
template<        class T       > struct my_enable_if<true, T> { typedef T type; };
template< int Value, typename = void > struct Printer {
static void doIt() {
std::cout << "Printer<" << Value << "(even)>::doIt()" << std::endl;
}
};
template< int Value > struct Printer< Value, typename my_enable_if<Value & 1>::type > {
static const char *c_prefix;
static void doIt() {
std::cout << "Printer<" << c_prefix << Value << "(odd)>::doIt()" << std::endl;
}
};
template<> const char *Printer<1>::c_prefix; // Declaration
template<> const char *Printer<3>::c_prefix; // Declaration
// or since C++17, inline static variable.
template<> inline const char *Printer<5>::c_prefix = "Five_";

在 cpp 中(可能分为几个 cpp,但只有一个定义):

template<> const char *Printer<1>::c_prefix = "One_";   // Definition
template<> const char *Printer<3>::c_prefix = "Three_"; // Definition

请注意,一个定义规则实际上有两种变体。

有些东西(大致是我们倾向于放在源文件中的内容)可能在整个程序中只定义一次:

  • 非内联命名空间范围变量
  • 非内联static类数据成员
  • 非内联函数
  • 定义上述任何一项的模板显式专业化
  • 模板显式实例化

其他具有外部链接的东西(大致是我们倾向于放在头文件中的内容)可以在不同的翻译单元中多次定义,只要所有定义都包含相同的标记并具有相同的含义:

  • 内联命名空间范围变量(在 C++17 及更高版本中)
  • 内联static类数据成员(在 C++17 及更高版本中)
  • 内联函数(包括在类定义中定义的函数)
  • 所有类型,包括classstructunionenumtypedef/using定义
  • 定义上述任何一项的模板显式专业化
  • 主模板
  • 类模板部分专用化
  • 模板演绎指南

(ODR 都不适用于命名空间。

在 C++14 及更早版本中,static类数据成员的每个显式专用化都属于第一类。 由于#include实质上是将头文件中的所有标记插入到翻译单元中,因此将显式专用化放在头中,因此两个翻译单元的格式都不正确,因此无需诊断。

因此,在 C++14 或更早版本中,您需要将该显式专用化定义移动到源文件。 但是,您还应该在类模板部分专用化之后尽快在头文件中声明所有显式专用化:

template<> const char *Printer<1>::c_prefix;
template<> const char *Printer<3>::c_prefix;
template<> const char *Printer<5>::c_prefix;

但是,在 C++17 及更高版本中,我们可以选择内联变量和内联static类数据成员。 因此,如果出于某种原因想在标头中保留显式专用化定义,则可以将其内联:

template<> inline const char *Printer<1>::c_prefix = "One_";

(顺便说一下,您的代码允许更改c_prefix指针以指向其他内容。如果不希望这样做,请考虑将类型从const char*更改为const char* const.)

我的印象是这个符号应该有弱联系 专门用于避免此错误。

你从哪里得到这个印象?

问题 1:为什么 Printer<1>::c_prefix 不是弱符号(在 g++ 下)?

再说一遍,为什么会这样?

现在,我的理解是这段代码应该编译而不是 调用任何未定义的行为

您违反了 ODR。Printer<1>::c_prefix的定义应该只出现在一个编译单元中,因为它是一个完整的专业化。

template<> const char *Printer<1>::c_prefix = "One_";

您要么混淆概念(可能与内联定义的弱联系),要么在某处有误解。弱链接是 ELF 格式的实现ABI 详细信息。在 gcc 中,您需要明确注释您希望具有弱链接的符号。您的代码与弱链接弱符号无关。