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

Posted wzzkaifa

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 不能有纯虚方法。

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

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

一场setTag引发的血案与思考

一场setTag引发的血案与思考

一行代码引发的”血案“!!!(软件开发项目管理skycto jeeditor)

一个脚本引发的血案

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