模板链接与前置声明引发的血案

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

首先在 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,从而 existstrue;否则如果不能隐式地转换为 ‘U’,就会调用重载的 Big Test(...) 返回 ‘Big’,从而 existsfalse

在这里,类型 Child * 被认为不能隐式转换为 ‘Base *’,导致了非期望的结果。至于为什么,下文会有分析。

objdump -S UsingBase.o

我们知道模板实例化只会在第一次用到的时候才会进行,接下来就穷追猛打,看看到底用Child *作为类型参数实例化了什么样的类。在这个示例代码中,有两处用到了Holder<Child*> * holderUsingBaseUsingChild,下面来分析它们。

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_ 仅有一字之差:名称中间的索引 Lb0Lb1。这个实例化版本才是期望被调用的版本。

那么问题就来了:

问题一:为什么有两个实例化版本,而链接到可执行程序中又只有一个版本?
问题二:为什么 UsingBase.o 中没能实例化出期望的版本?

问题解答

解答问题一

编译器会在每一个用到模板的编译单元中用实际模板参数进行实例化,这样在多个编译单元中可能会存在对相同模板参数的实例化版本,它们的符号命名中带有索引标识(如上面的 Lb0Lb1)。在链接阶段,编译会根据链接顺序剔除重复的实例化版本,最终针对每一个类模板参数只有一份实例化版本。在这里被剔除的实例化版本是 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.hUsingBase.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 ofclass 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 不能有纯虚方法。

以上是关于模板链接与前置声明引发的血案的主要内容,如果未能解决你的问题,请参考以下文章

模板链接与前置声明引发的血案

读Java虚拟机类加载引发的血案

一场setTag引发的血案与思考

一场setTag引发的血案与思考

一个脚本引发的血案

C# 一个数组未赋值引发的血案