如何从模板专业化中找到重复的定义?

How to find duplicate definitions from template specializations?

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

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

创建模板class:

// - 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
// --------------------------------

有一个 class,其中仅包含模板 class 定义,但 应该 包含专业化。

// - 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>();}
// --------------------------------

主要 class 现在知道 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;
}
// --------------------------------

请注意,这个问题只有在单独编译class A时才会出现,这个example from ideone输出6,而使用单独的文件会导致分段错误,或者输出42(https://ideone.com/3RTzlC ). 在我的机器上,示例编译成功并输出 2013265920:

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

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

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


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


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

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

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

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

Question 1: Why doesn't the linker detect this problem? This should be easy to spot by comparing the size of objects.

因为这不是链接器的问题。在模板的情况下,主要的 声明 和所有其他专业化(无论是 class 还是函数)应该是可见的 upfront.

Question 2: is there a way to examine the object files or the shared library to detect multiple implementations of the same type like in the example above?

至少我不知道。

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

// 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"

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

更新:
对同一个函数有多个 body 定义是 未定义的行为 。这就是为什么你会得到像 2013265920 这样的笨拙输出的原因,同样的情况也会发生在我的机器上。输出应该是 426。我给你上面的例子是因为使用 inline 函数你可以创建这样的竞争条件。

以我对链接阶段的有限了解,典型链接器承担的责任仅限于拒绝超过 1 个 non-inline 具有相同签名的函数定义 。例如

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

除此之外它不检查函数 body 是否相似,因为它已经在早期解析过并且链接器不解析函数 body.
上面说了,现在注意这句话:

template functions are always inline by nature

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

最安全的方法是 #include 将 read-only header 放入您的专用 header 中,并在所有地方包含该专用 header。

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

解决方法是将专业化放在定义主 class 模板的同一个头文件中。

Question 1: Why doesn't the linker detect this problem? This should be easy to spot by comparing the size of objects.

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

Question 2: is there a way to examine the object files or the shared library to detect multiple implementations of the same type like in the example above?

您应该使用 gold linker 来链接您的 C++ 应用程序,如果您还没有使用它的话。它的一个很好的特性是 --detect-odr-violations 命令行开关,它完全按照你的要求做:

gold uses a heuristic to find potential ODR violations: if the same symbol is seen defined in two different input files, and the two symbols have different sizes, then gold looks at the debugging information in the input objects. If the debugging information suggests that the symbols were defined in different source files, gold reports a potential ODR violation. This approach has both false negatives and false positives. However, it is reasonably reliable at detecting problems when linking unoptimized code. It is much easier to find these problems at link time than to debug cases where the wrong symbol.

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

我认为你设法欺骗了编译器。但是我应该指出,以我的拙见,您以错误的方式理解了模板的概念,尤其是您试图将模板专业化与继承混为一谈。 我的意思是,模板特化不能向 class 添加数据成员,唯一的目的是定义函数参数和 class 字段的类型。如果你想改变算法,即重写代码,或向 class 添加新的日期成员,你应该定义派生 class。 关于 "several-step" 库中的单独编译和模板 class,C++ 参考说 (http://www.cplusplus.com/doc/oldtutorial/templates/):

Because templates are compiled when required, this forces a restriction for multi-file projects: the implementation (definition) of a template class or function must be in the same file as its declaration. That means that we cannot separate the interface in a separate header file, and that we must include both interface and implementation in any file that uses the templates. Since no code is generated until a template is instantiated when required, compilers are prepared to allow the inclusion more than once of the same template file with both declarations and definitions in a project without generating linkage errors.