C++编译器优化会放弃模板特化。

3

我注意到如果启用编译器优化,.cpp文件中的模板特化将被丢弃。我在一个大型应用程序中发现了这个问题,并将其简化为一个简单的示例。

首先,在obj.h中定义一个新类:

#ifndef _OBJ_H_
#define _OBJ_H_

class Obj { };

#endif //_OBJ_H_

然后我在templates.h中定义了一个新的模板函数。
#ifndef _TEMPLATES_H_
#define _TEMPLATES_H_

template<typename T>
int get()
{
    return 0;
}

#endif //_TEMPLATES_H_

...在templates.cpp中针对类Obj的特化

#include "templates.h"
#include "obj.h"

template<>
int get<Obj>()
{
    return 1;
}

然后我从主函数中调用该函数:

#include <stdio.h>
#include "templates.h"
#include "obj.h"

int main()
{
    printf("Get: %d\n", get<Obj>());
    return 0;
}

使用不同的-O级别编译此示例会产生不同的输出。
$ g++ -o a main.cpp templates.cpp -O0
$ ./a
Get: 1

$ g++ -o a main.cpp templates.cpp -O2 #same with -O3, -O4, Os
$ ./a
Get: 0

使用clang替换g++也会发生同样的情况。我使用的是g++ 4.7.2和clang 3.4。
虽然我不是汇编专家,但通过查看生成的代码,我可以看到-O0版本定义了名为_Z3getI3ObjEiv的符号,它引用了特化模板,而优化版本则将所有内容都内联了(正如我所预期的那样)。
问题最终是通过将所有特化模板移到头文件中解决的,但我仍然很好奇:为什么会发生这种情况?起初我认为我遇到了未定义的行为,尽管如果是这种情况,clang和g++产生相同的结果就有些奇怪了。

1
问题在于main.cpp不知道Obj特化存在,因为它是在编译时解析的,这就是为什么板子倾向于放在头文件中的原因。 - mmmmmm
1
两个编译器给出相同结果并不奇怪,也不能说明定义行为。有时,常见的实现策略会使某些不确定行为表现得有点可预测。编译器作者并没有特意让带有未定义行为的程序以最壮观的方式崩溃。 - user395760
1
补充Mark的评论,实际上,C++要求在使用所有特化时都必须显式声明(但不一定定义),因此您可以将定义保留在templates.cpp中,但是您确实需要在头文件中声明特化。 - user743382
2个回答

2
任何函数一样,特化必须在使用之前在每个使用它的翻译单元中声明。否则,编译器不知道它的存在,并将使用通用模板(必要时实例化它)。
这可能意味着您最终会得到两个非相同的特化副本(您编写的一个和从模板实例化的一个)。根据链接器的确切操作,这可能导致调用其中一个或另一个,或者链接错误,或其他未定义的行为。
请注意,您不需要将特化的定义移动到头文件中;声明即可。

2
在调用点不可见的特化是错误的。顺便说一下,编译器不需要将其视为错误,并且可以按照它们的意愿处理它。
你应该将特化放入头文件中,但在C++11中,你可以使用外部单元来声明显式实例化 使用C++11的“extern”关键字
正如Sebastian所指出的那样:这违反了14.7.3/6中的要求:
“如果模板[...]被显式地特化,则该特化必须在每个使用该特化的隐式实例化会发生的翻译单元中的第一次使用之前被声明;不需要进行任何诊断。”
由于不需要进行任何诊断,违反此要求是未定义的行为。

具体而言,这违反了14.7.3/6中的要求:“如果一个模板[...]被显式地特化,则该特化必须在每个出现隐式实例化的首次使用之前在每个翻译单元中声明;不需要进行任何诊断。”由于不需要进行任何诊断,违反此要求将导致未定义行为。 - Sebastian Redl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接