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

时间:2018-03-22 21:56:58

标签: c++ template-specialization

我无法从这些代码的不同编译器中获得一致的行为,并试图弄清楚我可能做错了什么。基本设置有一个模板类(Printer),一个部分特化(对于奇数值整数),它添加了一个静态const成员。当我尝试以各种不同的方式定义静态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>用法,则其余的按预期编译,链接和执行。

在另一个编译器(Greenhills)上我得到的错误是:

[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>用法调用未定义行为的内容吗?

3 个答案:

答案 0 :(得分:4)

  

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

必须在使用之前声明专业化,否则程序形成不良(无需诊断)。

所以正确的方法是:

文件&#34; template_bug.h&#34;

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

答案 1 :(得分:4)

请注意,One Definition Rule实际上有两种变体。

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

  • 非内联命名空间范围变量
  • 非内联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*更改为{ {1}}。)

答案 2 :(得分:2)

  

我的印象是这个符号应该有微弱的联系   特别是为了避免这个错误。

你在哪里得到这种印象?

  

问题1:为什么打印机&lt; 1&gt; :: c_prefix不是弱符号(在g ++下)?

再次,为什么会这样?

  

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

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

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

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