如何从模板特化中查找重复定义?

时间:2015-01-08 09:25:21

标签: c++ templates shared-libraries

我有一个带有特化的模板类,在另一个文件中定义。因此,可以生成同一类的两个版本:一次通过替换模板参数,一次使用专门化。我目前的理解是,这可能导致同一类型的两个实例在内存中具有不同的大小,从而导致分段错误。

我创建了一个最小的示例,以下代码用于说明问题:

创建模板类:

// - templateexample.h ---------------
#ifndef TEMPLATEEXAMPLE_H
#define TEMPLATEEXAMPLE_H
template<typename T> class Example
{
    public:
        Example(){}
        int doWork() {return 42;}
};
#endif
// -----------------------------------

另一个文件中的模板专业化:

// - templatespecialization.h --------
#ifndef TEMPLATESPECIALIZATION_H
#define TEMPLATESPECIALIZATION_H    
#include "templateexample.h"
template<> class Example<int>
{
    public:
        Example() : a(0), b(1), c(2), d(3) {} 
        int doWork() {return a+b+c+d;}

    private:
        int a; //<== the specialized object will be larger in memory
        int b;
        int c;
        int d;
};
#endif
// --------------------------------

有一个只包含模板类定义的类,但应该包含特化。

// - a.h --------------------------
#ifndef A_H
#define A_H   
#include "templateexample.h"
class A
{
    public:
        Example<int> returnSmallExample();
};
#endif

// - a.cpp ------------------------
#include "a.h"
Example<int> A::returnSmallExample() {return Example<int>();}
// --------------------------------

主类现在知道两个版本的Example<int>来自A和来自templatespecialization.h的版本。

// - main.cpp ---------------------
#include <iostream>
#include "a.h"
#include "templatespecialization.h"

int main()
{
    A a;
    Example<int> test = a.returnSmallExample();
    std::cout<<test.doWork()<<std::endl;
}
// --------------------------------

请注意,此问题仅在单独编译A类时发生,此示例来自ideone输出6,而使用单独的文件可能导致分段失败或输出42(https://ideone.com/3RTzlC)。 在我的机器上,该示例成功编译并输出2013265920

proof

在上面示例的生产版本中,所有内容都构建到main使用的共享库中。

问题1:为什么链接器没有检测到这个问题?通过比较对象的大小,这很容易被发现。

问题2:有没有办法检查目标文件或共享库,以检测同一类型的多个实现,如上例所示?


编辑:请注意:上面的代码是解释问题的最小示例。出现这种情况的原因是模板类来自一个库,我无法编辑此库中的文件。最后整个事情在可执行文件中的所有地方都被使用了,现在我需要找出上面的问题是否发生。


编辑:上面的代码可以像这样编译:

#!/bin/bash 
g++ -g -c a.cpp 
g++ -g -c main.cpp 
g++ -o test a.o main.o 

4 个答案:

答案 0 :(得分:7)

您在不同的翻译单元中对同一模板及其专业化有不同的定义。这会导致One Definition Rule违规。

修复方法是将专门化放在定义主类模板的同一个头文件中。

  

问题1:为什么链接器没有检测到这个问题?通过比较对象的大小,这很容易被发现。

不同的类型可能具有相同的大小(例如doubleint64_t),因此,显然,只是比较对象的大小不起作用。

  

问题2:有没有办法检查目标文件或共享库,以检测同一类型的多个实现,如上例所示?

如果您不使用C ++应用程序,则应使用gold linker链接C ++应用程序。它的一个很好的功能是--detect-odr-violations命令行开关,它完全符合您的要求:

  

gold使用启发式方法来查找潜在的ODR违规:如果在两个不同的输入文件中看到相同的符号,并且这两个符号的大小不同,那么gold会查看输入对象中的调试信息。如果调试信息表明符号是在不同的源文件中定义的,则gold会报告潜在的ODR违规。这种方法既有假阴性也有误报。但是,在链接未优化代码时检测问题是相当可靠的。在链接时找到这些问题要比调试错误符号的情况容易得多。

有关详细信息,请参阅Enforcing One Definition Rule

答案 1 :(得分:1)

  

问题1 :为什么链接器没有检测到这个问题?通过比较对象的大小,这很容易被发现。

因为这不是链接器的问题。如果是模板,主声明和所有其他专业化(无论是类还是函数)应该是可见的 upfront

  

问题2 :有没有办法检查目标文件或共享库以检测相同类型的多个实现   就像上面的例子一样?

至少我不知道。

为了进一步简化这种情况,请查看类似的损坏代码:

// foo.h
inline void foo () {
#ifdef FOO
  return;
#else
  throw 0;
#endif
}

// foo1.cpp
#define FOO
#include"foo.h"
// ... uses foo() with "return"

// foo2.cpp
#include"foo.h"
// ... uses foo() with "throw"

根据使用的编译方式,您可能得到不同的结果。

<强>更新
具有相同功能的多个主体定义是未定义的行为。这就是为什么你得到像2013265920这样笨拙的输出的原因,同样在我的机器上也是如此。输出应为426。我给出了上面的例子,因为使用inline函数可以创建这样的竞争条件。

由于我对链接阶段的了解有限,典型链接器所承担的责任仅限于拒绝具有相同签名的多于一个非内联函数定义。 e.g。

// Header.h is included in multiple .cpp files
void foo () {}  // rejected due to multiple definitions
inline void bar () {}  // ok because `inline` keyword is found

除此之外,它不检查函数体是否相似,因为在早期阶段已经解析了它,并且链接器不解析函数体。
如上所述,现在注意这个说法:

  

template函数本质上总是inline

因此,链接器可能没有机会拒绝它们。

最安全的方法是将#include只读标头添加到您的专用标头中,并在任何地方包含该专用标头。

答案 2 :(得分:0)

我不知道通过分析已编译的二进制文件来做到这一点的方法,但您可以构建一个程序的#include关系图 - 有些工具可以做到这一点,例如Doxygen - 并使用它来查找(直接或间接)包含库头但不包含特化头的文件。

您需要检查每个文件以确定它是否实际使用了相关模板,但至少可以缩小您必须检查的文件集。

答案 3 :(得分:0)

我认为你设法欺骗了编译器。但我应该注意到,在我的拙见中,你以错误的方式理解模板的概念,特别是你试图将模板专业化与继承混合在一起。 我的意思是,模板专门化不能将数据成员添加到类中,唯一的目标是定义函数参数和类字段的类型。如果要更改算法,即重写代码或将新的日期成员添加到类,则应定义派生类。 关于&#34;几步&#34;在库中单独编译和模板类,C ++参考说(http://www.cplusplus.com/doc/oldtutorial/templates/):

  

由于模板是在需要时编译的,因此会强制限制多文件项目:模板类或函数的实现(定义)必须与其声明位于同一文件中。这意味着我们无法在单独的头文件中分离接口,并且我们必须在使用模板的任何文件中包含接口和实现。   由于在需要时实例化模板之前不会生成代码,因此编译器准备允许在项目中包含多个声明和定义的同一模板文件不止一次,而不会产生链接错误。