模板链接与前置声明引发的血案
Posted 飘飘白云
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了模板链接与前置声明引发的血案相关的知识,希望对你有一定的参考价值。
模板链接与前置声明引发的血案
现象:
有一个类模板,它会根据模板类型参数T
的实际类型,调用不同的实例化泛型函数子去处理实际事情。在程序运行时,发现在不同的模块中用相同的类型参数来调用该类模板,得到的结果不一致,也就是说在传入同样的实际模板类型参数实例化了不同的泛型函数子。因此,可以推测在不同的模块中对同样的实际模板类型参数作了不一样的处理,导致生成了不一样的实例化。
问题原型:
为了方便描述,我写了一个能重现这个问题的简化版原型:点此下载源码
模板参数类型类
Base类:
// Base.h
//
class Base
public:
virtual ~Base();
virtual const char* GetName();
;
// Base.cpp
//
#include "Base.h"
Base::~Base()
const char* Base::GetName()
return "Base";
Child类:
// Child.h
//
#include "Base.h"
class Child : public Base
public:
virtual const char* GetName();
;
// Child.cpp
//
#include "Child.h"
const char* Child::GetName()
return "Child";
VisibleChild类:
// VisibleChild.h
//
#include "Base.h"
class VisibleChild : public Base
public:
virtual const char* GetName();
;
// VisibleChild.cpp
//
#include "VisibleChild.h"
const char* VisibleChild::GetName()
return "VisibleChild";
使用类模板的类
UsingBase类:
// UsingBase.h
//
#include "Template.h"
#include "Base.h"
class Child;
class VisibleChild;
class UsingBase
public:
void Use();
private:
void Print(Holder<Base*> * holder);
void Print(Holder<Child*> * holder);
void Print(Holder<VisibleChild*> * holder);
;
// UsingBase.cpp
//
#include "UsingBase.h"
#include "VisibleChild.h"
void UsingBase::Print(Holder<Base*> * holder)
holder->Print();
void UsingBase::Print(Holder<Child*> * holder)
holder->Print();
void UsingBase::Print(Holder<VisibleChild*> * holder)
holder->Print();
void UsingBase::Use()
printf("\\n=== UsingBase::Use() ===\\n");
Base* base = new Base();
Holder<Base*>* hb = new Holder<Base*>(base);
Print(hb);
delete base;
delete hb;
VisibleChild* visibleChild = new VisibleChild();
Holder<VisibleChild*>* hc2 = new Holder<VisibleChild*>(visibleChild);
Print(hc2);
delete visibleChild;
delete hc2;
UsingChild类:
// UsingChild.h
//
#include "Template.h"
class Child;
class VisibleChild;
class UsingChild
public:
void Use();
private:
void Print(Holder<Child*> * holder);
void Print(Holder<VisibleChild*> * holder);
;
// UsingChild.cpp
//
#include "UsingChild.h"
#include "Child.h"
#include "VisibleChild.h"
void UsingChild::Print(Holder<Child*> * holder)
holder->Print();
void UsingChild::Print(Holder<VisibleChild*> * holder)
holder->Print();
void UsingChild::Use()
printf("\\n=== UsingChild::Use() ===\\n");
Child* child = new Child();
Holder<Child*>* hc = new Holder<Child*>(child);
Print(hc);
delete child;
delete hc;
VisibleChild* visibleChild = new VisibleChild();
Holder<VisibleChild*>* hc2 = new Holder<VisibleChild*>(visibleChild);
Print(hc2);
delete visibleChild;
delete hc2;
类模板:
// Template.h
//
#include <stdio.h>
#include "Base.h"
// Helper types Small and Big - guarantee that sizeof(Small) < sizeof(Big)
//
template <class T, class U>
struct ConversionHelper
typedef char Small;
struct Big char dummy[2]; ;
static Big Test(...);
static Small Test(U);
static T & MakeT();
;
// class template Conversion
// Figures out the conversion relationships between two types
// Invocations (T and U are types):
// exists: returns (at compile time) true if there is an implicit conversion
// from T to U (example: Derived to Base)
// Caveat: might not work if T and U are in a private inheritance hierarchy.
//
template <class T, class U>
struct Conversion
typedef ConversionHelper<T, U> H;
enum
exists = sizeof(typename H::Small) == sizeof((H::Test(H::MakeT())))
;
enum exists2Way = exists && Conversion<U, T>::exists ;
enum sameType = false ;
;
template <class T>
class Conversion<T, T>
public:
enum exists = true, exists2Way = true, sameType = true ;
;
#ifndef SUPERSUBCLASS
#define SUPERSUBCLASS(Super, Sub) \\
(Conversion<Sub, Super>::exists && !Conversion<Super, void*>::sameType)
#endif
template<class T, bool isTypeOfBase>
struct ProcessFunc
void operator()(T obj)
printf("It's not type of Base.\\n");
;
template<class T>
struct ProcessFunc<T, true>
void operator()(T obj)
printf("It's type of Base. GetName: %s\\n", obj->GetName());
;
template <class T>
class Holder
public:
Holder(T obj)
: mValue(obj)
void Print()
ProcessFunc<T, SUPERSUBCLASS(Base*, T) > func;
func(mValue);
private:
T mValue;
;
main():
#include "UsingBase.h"
#include "UsingChild.h"
int main()
UsingBase ub;
ub.Use();
UsingChild uc;
uc.Use();
return 0;
运行结果:
=== UsingBase::Use() ===
It's type of Base. GetName: Base
It's type of Base. GetName: VisibleChild
=== UsingChild::Use() ===
It's not type of Base.
It's type of Base. GetName: VisibleChild
在 UsingChild::Use()
中,用子类型 Child *
作为类型参数时,类模板没能”正确”实例化,导致它调用了非偏特化的 ProcessFunc
函数子,这不是期望的结果。而用子类型 VisibleChild *
作为类型参数时,类模板正确实例化,得到了我们期望的结果。
分析
为了验证前面的推测:在不同的模块中对同样的实际模板类型参数作了不一样的处理,导致生成了不一样的实例化。下面来分析代码的实际执行过程。
在linux
下,可以用 objdump -S
来查看目标文件或可执行文件的源码与汇编代码对应关系。首先我们来分析可执行文件:TemplateLink
。
objdump -S TemplateLink
首先在 UsingChild::Use() 找到用Child类型作为模板参数的调用点:
0000000000400ba0 <_ZN10UsingChild3UseEv>:
void UsingChild::Use()
400ba0: 55 push %rbp
400ba1: 48 89 e5 mov %rsp,%rbp
400ba4: 53 push %rbx
400ba5: 48 83 ec 38 sub $0x38,%rsp
400ba9: 48 89 7d c8 mov %rdi,-0x38(%rbp)
printf("\\n=== UsingChild::Use() ===\\n");
400bad: bf 88 0f 40 00 mov $0x400f88,%edi
400bb2: e8 c9 fa ff ff callq 400680 <puts@plt>
Child* child = new Child();
400bb7: bf 08 00 00 00 mov $0x8,%edi
400bbc: e8 ef fa ff ff callq 4006b0 <_Znwm@plt>
400bc1: 48 89 c3 mov %rax,%rbx
400bc4: 48 c7 03 00 00 00 00 movq $0x0,(%rbx)
400bcb: 48 89 df mov %rbx,%rdi
400bce: e8 e5 00 00 00 callq 400cb8 <_ZN5ChildC1Ev>
400bd3: 48 89 5d d0 mov %rbx,-0x30(%rbp)
Holder<Child*>* hc = new Holder<Child*>(child);
400bd7: bf 08 00 00 00 mov $0x8,%edi
400bdc: e8 cf fa ff ff callq 4006b0 <_Znwm@plt>
400be1: 48 89 c3 mov %rax,%rbx
400be4: 48 8b 45 d0 mov -0x30(%rbp),%rax
400be8: 48 89 c6 mov %rax,%rsi
400beb: 48 89 df mov %rbx,%rdi
400bee: e8 eb 00 00 00 callq 400cde <_ZN6HolderIP5ChildEC1ES1_>
400bf3: 48 89 5d d8 mov %rbx,-0x28(%rbp)
Print(hc);
400bf7: 48 8b 55 d8 mov -0x28(%rbp),%rdx
400bfb: 48 8b 45 c8 mov -0x38(%rbp),%rax
400bff: 48 89 d6 mov %rdx,%rsi
400c02: 48 89 c7 mov %rax,%rdi
400c05: e8 5a ff ff ff callq 400b64 <_ZN10UsingChild5PrintEP6HolderIP5ChildE>
delete child;
400c0a: 48 83 7d d0 00 cmpq $0x0,-0x30(%rbp)
400c0f: 74 17 je 400c28 <_ZN10UsingChild3UseEv+0x88>
400c11: 48 8b 45 d0 mov -0x30(%rbp),%rax
400c15: 48 8b 00 mov (%rax),%rax
400c18: 48 83 c0 08 add $0x8,%rax
400c1c: 48 8b 00 mov (%rax),%rax
400c1f: 48 8b 55 d0 mov -0x30(%rbp),%rdx
400c23: 48 89 d7 mov %rdx,%rdi
400c26: ff d0 callq *%rax
delete hc;
400c28: 48 8b 45 d8 mov -0x28(%rbp),%rax
400c2c: 48 89 c7 mov %rax,%rdi
400c2f: e8 5c fa ff ff callq 400690 <_ZdlPv@plt>
这个调用点就是 Print(hc)
,C++默认是将this作为第一个参数,所以源码中的hc->Print()
在这里就对应C形式的Print(hc)
。找到其对应的符号_ZN10UsingChild5PrintEP6HolderIP5ChildE
,然后使用这个符号在dump信息中找到对应的代码:
0000000000400b64 <_ZN10UsingChild5PrintEP6HolderIP5ChildE>:
#include "UsingChild.h"
#include "Child.h"
#include "VisibleChild.h"
void UsingChild::Print(Holder<Child*> * holder)
400b64: 55 push %rbp
400b65: 48 89 e5 mov %rsp,%rbp
400b68: 48 83 ec 10 sub $0x10,%rsp
400b6c: 48 89 7d f8 mov %rdi,-0x8(%rbp)
400b70: 48 89 75 f0 mov %rsi,-0x10(%rbp)
holder->Print();
400b74: 48 8b 45 f0 mov -0x10(%rbp),%rax
400b78: 48 89 c7 mov %rax,%rdi
400b7b: e8 d4 fe ff ff callq 400a54 <_ZN6HolderIP5ChildE5PrintEv>
400b80: c9 leaveq
400b81: c3 retq
在这里是转调holder->Print();
,找到其对应的符号_ZN6HolderIP5ChildE5PrintEv
,然后使用这个符号在dump信息中找到对应的代码:
0000000000400a54 <_ZN6HolderIP5ChildE5PrintEv>:
public:
Holder(T obj)
: mValue(obj)
void Print()
400a54: 55 push %rbp
400a55: 48 89 e5 mov %rsp,%rbp
400a58: 48 83 ec 20 sub $0x20,%rsp
400a5c: 48 89 7d e8 mov %rdi,-0x18(%rbp)
ProcessFunc<T, SUPERSUBCLASS(Base*, T) > func;
func(mValue);
400a60: 48 8b 45 e8 mov -0x18(%rbp),%rax
400a64: 48 8b 10 mov (%rax),%rdx
400a67: 48 8d 45 ff lea -0x1(%rbp),%rax
400a6b: 48 89 d6 mov %rdx,%rsi
400a6e: 48 89 c7 mov %rax,%rdi
400a71: e8 96 00 00 00 callq 400b0c <_ZN11ProcessFuncIP5ChildLb0EEclES1_>
400a76: c9 leaveq
400a77: c3 retq
在这里是根据模板参数类型实例化的泛型函数子来分发的:ProcessFunc<T, SUPERSUBCLASS(Base*, T) > func; func(mValue);
,找到其对应的符号_ZN11ProcessFuncIP5ChildLb0EEclES1_
,然后使用这个符号在dump信息中找到最终执行的代码:
0000000000400b0c <_ZN11ProcessFuncIP5ChildLb0EEclES1_>:
template<class T, bool isTypeOfBase>
struct ProcessFunc
void operator()(T obj)
400b0c: 55 push %rbp
400b0d: 48 89 e5 mov %rsp,%rbp
400b10: 48 83 ec 10 sub $0x10,%rsp
400b14: 48 89 7d f8 mov %rdi,-0x8(%rbp)
400b18: 48 89 75 f0 mov %rsi,-0x10(%rbp)
printf("It's not type of Base.\\n");
400b1c: bf 70 0f 40 00 mov $0x400f70,%edi
400b21: e8 5a fb ff ff callq 400680 <puts@plt>
400b26: c9 leaveq
400b27: c3 retq
从这段代码可以看到:用Child *
类型作为类模板参数时,实例化了非偏特化的泛型函数子ProcessFunc
,从而显示了非期望的结果It's not type of Base.\\n
。
用同样的方式,可以找到用VisibleChild
类型作为类模板参数时,实例化了的偏特化的泛型函数子ProcessFunc
:
0000000000400b28 <_ZN11ProcessFuncIP12VisibleChildLb1EEclES1_>:
;
template<class T>
struct ProcessFunc<T, true>
void operator()(T obj)
400b28: 55 push %rbp
400b29: 48 89 e5 mov %rsp,%rbp
400b2c: 48 83 ec 10 sub $0x10,%rsp
400b30: 48 89 7d f8 mov %rdi,-0x8(%rbp)
400b34: 48 89 75 f0 mov %rsi,-0x10(%rbp)
printf("It's type of Base. GetName: %s\\n", obj->GetName());
400b38: 48 8b 45 f0 mov -0x10(%rbp),%rax
400b3c: 48 8b 00 mov (%rax),%rax
400b3f: 48 83 c0 10 add $0x10,%rax
400b43: 48 8b 00 mov (%rax),%rax
400b46: 48 8b 55 f0 mov -0x10(%rbp),%rdx
400b4a: 48 89 d7 mov %rdx,%rdi
400b4d: ff d0 callq *%rax
400b4f: 48 89 c6 mov %rax,%rsi
400b52: bf 50 0f 40 00 mov $0x400f50,%edi
400b57: b8 00 00 00 00 mov $0x0,%eax
400b5c: e8 ff fa ff ff callq 400660 <printf@plt>
400b61: c9 leaveq
400b62: c3 retq
400b63: 90 nop
至此,可以推断分别用Child *
和VisibleChild *
作为类模板参数时,导致了对另一个类模板参数 bool isTypeOfBase
的不同推导结果。对于Child *
类型来说:SUPERSUBCLASS(Base*, Child*)
推导为false
;而对于VisibleChild *
类型来说:SUPERSUBCLASS(Base*, VisibleChild*)
推导为’true’。它们都是Base
的子类,却推导出不同的结果,何其诡异呀!
SUPERSUBCLASS 分析
SUPERSUBCLASS
是一个宏:
#ifndef SUPERSUBCLASS
#define SUPERSUBCLASS(Super, Sub) \\
(Conversion<Sub, Super>::exists && !Conversion<Super, void*>::sameType)
#endif
它返回 Sub
是否可以隐式转换为 Super
类型,且 Super
不得是 void*
类型。这个转换判断操作是泛型类 Conversion
来完成的:
template <class T, class U>
struct ConversionHelper
typedef char Small;
struct Big char dummy[2]; ;
static Big Test(...);
static Small Test(U);
static T & MakeT();
;
template <class T, class U>
struct Conversion
typedef ConversionHelper<T, U> H;
enum
exists = sizeof(typename H::Small) == sizeof((H::Test(H::MakeT())))
;
enum exists2Way = exists && Conversion<U, T>::exists ;
enum sameType = false ;
;
template <class T>
class Conversion<T, T>
public:
enum exists = true, exists2Way = true, sameType = true ;
;
#endif
如果MakeT()
返回的类型参数 T
能够隐式地转换为 ‘U’,那么就会调用 ‘Small Test(U)’ 返回 Small
,从而 exists
为 true
;否则如果不能隐式地转换为 ‘U’,就会调用重载的 Big Test(...)
返回 ‘Big’,从而 exists
为 false
。
在这里,类型 Child *
被认为不能隐式转换为 ‘Base *’,导致了非期望的结果。至于为什么,下文会有分析。
objdump -S UsingBase.o
我们知道模板实例化只会在第一次用到的时候才会进行,接下来就穷追猛打,看看到底用Child *
作为类型参数实例化了什么样的类。在这个示例代码中,有两处用到了Holder<Child*> * holder
: UsingBase
和 UsingChild
,下面来分析它们。
Disassembly of section .text._ZN11ProcessFuncIP5ChildLb0EEclES1_:
0000000000000000 <_ZN11ProcessFuncIP5ChildLb0EEclES1_>:
template<class T, bool isTypeOfBase>
struct ProcessFunc
void operator()(T obj)
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 48 89 7d f8 mov %rdi,-0x8(%rbp)
c: 48 89 75 f0 mov %rsi,-0x10(%rbp)
printf("It's not type of Base.\\n");
10: bf 00 00 00 00 mov $0x0,%edi
15: e8 00 00 00 00 callq 1a <_ZN11ProcessFuncIP5ChildLb0EEclES1_+0x1a>
1a: c9 leaveq
1b: c3 retq
注意看符号 _ZN11ProcessFuncIP5ChildLb0EEclES1_
,这正是前面非期望情况下调用的版本。也就是说用Child *
作为模板类型参数最终调用的是这个实例化版本。
objdump -S UsingChild.o
Disassembly of section .text._ZN11ProcessFuncIP5ChildLb1EEclES1_:
0000000000000000 <_ZN11ProcessFuncIP5ChildLb1EEclES1_>:
template<class T>
struct ProcessFunc<T, true>
void operator()(T obj)
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 48 89 7d f8 mov %rdi,-0x8(%rbp)
c: 48 89 75 f0 mov %rsi,-0x10(%rbp)
printf("It's type of Base. GetName: %s\\n", obj->GetName());
10: 48 8b 45 f0 mov -0x10(%rbp),%rax
14: 48 8b 00 mov (%rax),%rax
17: 48 83 c0 10 add $0x10,%rax
1b: 48 8b 00 mov (%rax),%rax
1e: 48 8b 55 f0 mov -0x10(%rbp),%rdx
22: 48 89 d7 mov %rdx,%rdi
25: ff d0 callq *%rax
27: 48 89 c6 mov %rax,%rsi
2a: bf 00 00 00 00 mov $0x0,%edi
2f: b8 00 00 00 00 mov $0x0,%eax
34: e8 00 00 00 00 callq 39 <_ZN11ProcessFuncIP5ChildLb1EEclES1_+0x39>
39: c9 leaveq
3a: c3 retq
注意看符号 _ZN11ProcessFuncIP5ChildLb1EEclES1_
,它和上面objdump -S UsingBase.o
中的符号 _ZN11ProcessFuncIP5ChildLb0EEclES1_
仅有一字之差:名称中间的索引 Lb0
和 Lb1
。这个实例化版本才是期望被调用的版本。
那么问题就来了:
问题一:为什么有两个实例化版本,而链接到可执行程序中又只有一个版本?
问题二:为什么UsingBase.o
中没能实例化出期望的版本?
问题解答
解答问题一
编译器会在每一个用到模板的编译单元中用实际模板参数进行实例化,这样在多个编译单元中可能会存在对相同模板参数的实例化版本,它们的符号命名中带有索引标识(如上面的 Lb0
和 Lb1
)。在链接阶段,编译会根据链接顺序剔除重复的实例化版本,最终针对每一个类模板参数只有一份实例化版本。在这里被剔除的实例化版本是 UsingChild
中的_ZN11ProcessFuncIP5ChildLb1EEclES1_
,而留下的是 UsingBase
中的 _ZN11ProcessFuncIP5ChildLb0EEclES1_
。
解答问题二
在 UsingBase
中,对于Child *
类型来说:SUPERSUBCLASS(Base*, Child*)
被推导为 false
。为什么会这样呢?再来仔细看看 UsingBase
的实现:
// UsingBase.h
//
#include "Template.h"
#include "Base.h"
class Child;
class VisibleChild;
class UsingBase
public:
void Use();
private:
void Print(Holder<Base*> * holder);
void Print(Holder<Child*> * holder);
void Print(Holder<VisibleChild*> * holder);
;
// UsingBase.cpp
//
#include "UsingBase.h"
#include "VisibleChild.h"
void UsingBase::Print(Holder<Base*> * holder)
holder->Print();
void UsingBase::Print(Holder<Child*> * holder)
holder->Print();
void UsingBase::Print(Holder<VisibleChild*> * holder)
holder->Print();
void UsingBase::Use()
printf("\\n=== UsingBase::Use() ===\\n");
Base* base = new Base();
Holder<Base*>* hb = new Holder<Base*>(base);
Print(hb);
delete base;
delete hb;
VisibleChild* visibleChild = new VisibleChild();
Holder<VisibleChild*>* hc2 = new Holder<VisibleChild*>(visibleChild);
Print(hc2);
delete visibleChild;
delete hc2;
可以看到在 UsingBase
这个编译单元中,Child
只有前置声明,它是一个外部未定义的符号,看不到它的类型信息。因此 Child *
被当做普通的指针看待,因而 Conversion<Child *, Base *>::exists
被推导为 false,从而实例化了非偏特化的 ProcessFunc
版本,产生了问题。如果需要达到期望的效果,就必须看到 Child
的完整类型信息。
解决方案
针对这个 Child
个例,可以在 UsingBase.h
或 UsingBase.cpp
中添加头文件来消除这个 bug。但这并非通用的解决方案,因为没有根本解决泛型函数子 ProcessFunc<T, SUPERSUBCLASS(Base*, T) >
第二个参数正确推导的问题,也就是说我们需要逼着模板类型参数 T 提前显示它的完整类型信息。如果我们修改为某种类似 SUPERSUBCLASS(Base, Child)
的判断方式,就可以达到这一目的。这是可以实现的,通过使用类型萃取技法,我们可以从模板参数 T 萃取它包含的裸类型(bare type)或值类型。
类型萃取辅助类:
// Helper traits get bared type
//
template <typename T>
struct TypeTraitsItem
typedef T BaredType;
enum isPointer = false ;
;
template <typename T>
struct TypeTraitsItem<T*>
typedef T BaredType;
enum isPointer = true ;
;
template <typename T>
struct TypeTraitsItem<const T*>
typedef T BaredType;
enum isPointer = true ;
;
template <typename T>
struct TypeTraits
typedef typename TypeTraitsItem<T>::BaredType BaredType;
enum isPointer = TypeTraitsItem<T>::isPointer ;
;
应用
修改之后的 Holder :
#include "TypeOp.h"
template <class T>
class Holder
public:
Holder(T obj)
: mValue(obj)
void Print()
typedef typename TypeTraits<T>::BaredType CompleteType;
ProcessFunc<T, SUPERSUBCLASS(Base, CompleteType) > func;
func(mValue);
private:
T mValue;
;
做出修改这样的修改之后,再次编译运行,就会得到编译错误信息:
../Template.h: In instantiation of ‘struct Conversion<Child, Base>’:
../Template.h:85:24: required from ‘void Holder<T>::Print() [with T = Child*]’
../UsingBase.cpp:19:19: required from here
../Template.h:39:73: error: invalid use of incomplete type ‘class Child’
exists = sizeof(typename H::Small) == sizeof((H::Test(H::MakeT())))
^
In file included from ../UsingBase.cpp:8:0:
../UsingBase.h:14:7: error: forward declaration of ‘class Child’
class Child;
^
make: *** [UsingBase.o] Error 1
这样就能将问题提前抛出,从而定位出需要修改的地方。
不足
这种提前抛出问题的解决方案,并非完美,因为它是通过将判断 Conversion<Child *, Base *>::exists
转换为判断 Conversion<Child, Base>::exists
来实现的,而 T & MakeT() 或
Small Test(U)` 对后者有更严格的限制:必须能够存在 T 的对象和 U 的对象,也就是说 T 和 U 不能有纯虚方法。
以上是关于模板链接与前置声明引发的血案的主要内容,如果未能解决你的问题,请参考以下文章