你好,我是雨乐!
在上一篇文章<<惯用法之CRTP>>(如果不了解什么是CRTP,请先阅读该篇文章😁)一文中,介绍了CRTP的基本原理。今天借助本文,总结下在开发过程中,使用CRTP遇到的坑。
CRTP技术因为其性能优越,实现简单,在工程应用中非常广泛。实际上,相对于普通的虚函数,其具有一定的局限性。问题在于Base类实际上是一个模板类,而不是一个实际的类。因此,如果存在名为Derived和Derived1
的派生类,则基类模板初始化将具有不同的类型。这是因为,Base类将派生自不同的特化,即 Base,代码如下:
#include <iostream>
#include <string>
template <typename T>
class Base{
public:
void interface(){
static_cast<T*>(this)->imp();
}
void imp(){
std::cout << "in Base::imp" << std::endl;
}
};
class Derived : public Base<Derived> {
void imp(){
std::cout << "in Derived::imp" << std::endl;
}
};
class Derived1 : public Base<Derived1> {
void imp(){
std::cout << "in Derived1::imp" << std::endl;
}
};
int main() {
Base<Derived> *b = new Derived;
Base<Derived> *b1 = new Derived1;
auto vec = {d, d1}; // 出错
return 0;
}
在上述示例中,程序会输出如下:
In function ‘int main()’:
test.cc:39:20: error: unable to deduce ‘std::initializer_list<_Tp>’ from ‘{d, d1}’
auto vec = {d, d1};
从上面内容可以看出,vec类型推导失败,这是因为d和d1属于不同的类型,因此不能将CRTP对象或者指针放入容器中
。
首先,我们看一个例子:
#include <iostream>
#include <typeinfo>
#include <sys/time.h>
template<typename T>
class Base {
public:
void PrintType() {
T &t = static_cast<T&>(*this);
t.PrintType();
}
};
class Derived : public Base<Derived> {
// 此处没有实现PrintType()函数
};
int main() {
Derived d;
d.PrintType();
return 0;
}
编译并运行之后,输出如下:
Segmentation fault
是不是感觉很奇怪,单分析代码,没看出什么问题来,于是借助gdb来进行分析,如下:
#124 0x00000000004006c4 in Base<Derived>::PrintType (this=0x7fffffffe38f)
at crtp.cc:11
#125 0x00000000004006c4 in Base<Derived>::PrintType (this=0x7fffffffe38f)
at crtp.cc:11
#126 0x00000000004006c4 in Base<Derived>::PrintType (this=0x7fffffffe38f)
at crtp.cc:11
#127 0x00000000004006c4 in Base<Derived>::PrintType (this=0x7fffffffe38f)
at crtp.cc:11
#128 0x00000000004006c4 in Base<Derived>::PrintType (this=0x7fffffffe38f)
at crtp.cc:11
#129 0x00000000004006c4 in Base<Derived>::PrintType (this=0x7fffffffe38f)
at crtp.cc:11
#130 0x00000000004006c4 in Base<Derived>::PrintType (this=0x7fffffffe38f)
at crtp.cc:11
#131 0x00000000004006c4 in Base<Derived>::PrintType (this=0x7fffffffe38f)
at crtp.cc:11
从上述gdb的分析结果看出,重复执行crtp.cc中第11,即递归调用t.PrintType()
。那么为什么会出现这种递归调用这种现象呢?
在上一篇文章中,有提到,如果派生类没有实现某个基类中定义的函数,那么调用的是基类的函数。听起来比较绕口,我们以上述例子为例进行分析:
正是因为以上几点,所以才导致了这种递归调用引起的堆栈溢出。
那么,如何避免此类问题呢?可以使用下述方式实现:
template<typename T>
class Base {
public:
void PrintType() {
T &t = static_cast<T&>(*this);
t.PrintTypeImpl();
}
void PrintTypeImpl() {}
};
class Derived : public Base<Derived> {
// 此处没有实现PrintTypeImpl()函数
};
int main() {
Derived d;
d.PrintType();
return 0;
}
在上述方案中,在基类中重新定义了另外一个函数PrintTypeImpl()
,这样在调用PrintType()的时候,如果派生类中没有实现PrintTypeImpl()函数,则会调用基类的PrintTypeImpl()函数,这样就避免了因为递归调用而导致的堆栈溢出问题。
CRTP可以带来性能上的好处,但前提是我们写的代码真的遵守了那个规范。要是我们因为笔误写错了代码了呢?比如这样:
class Derived1 : public Base<Derived> { //此处有笔误
};
按照CRTP的要求,class Derived1应该继承的是 Base<Derived1>而不是Base<Derived>
。如果笔误写成上述这样,在基类 Base()
通过 static_cast
之后有可能有不预期行为发生的。
为了尽量将上述笔误尽可能早的暴露出来,我们可以使用下面这张方式:根据继承规则,派生类初始化时一定会先调用基底类的构造函数,所以我们就将基类的构造函数声明为private
,并且,利用 friend
修饰符的特点,即只有继承的子类 T
可以访问这个私有构造函数。其它的类如果想要访问这个私有构造函数,就会在编译期报错,如此做法,可以将问题暴露在编译阶段。
即将基类Base重新定义为如下格式:
template<typename T>
class Base {
public:
virtual void PrintType() const {
std::cout << typeid(*this).name() << std::endl;
}
private:
Base() = default;
friend T;
};
经过上述修改,Base中只能Derived类访问Base类的构造函数,而Derived1是不能访问Base类构造函数的,因此在编译阶段失败。
如上代码,编译的时候,会提示如下报错:
test.cc: In function ‘int main()’:
test.cc:39:12: error: use of deleted function ‘Derived1::Derived1()’
Derived1 d1;
^
test.cc:24:7: note: ‘Derived1::Derived1()’ is implicitly deleted because the default definition would be ill-formed:
class Derived1 : public Base<Derived> {
^
test.cc:12:5: error: ‘Base<T>::Base() [with T = Derived]’ is private
Base() = default;
^
test.cc:24:7: error: within this context
class Derived1 : public Base<Derived> {
好了,今天的文章就到这,我们下期见!