模板链接与前置声明引发的血案
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
。
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 不能有纯虚方法。
以上是关于模板链接与前置声明引发的血案的主要内容,如果未能解决你的问题,请参考以下文章