这个主题要求的是什么。我还想知道为什么通常的CRTP示例都没有提到virtual
析构函数。
编辑: 各位,请也发表关于CRTP问题的帖子,谢谢。
只有虚函数需要动态分派(因此需要 vtable 查找),而且并非在所有情况下都需要。如果编译器能够在编译时确定方法调用的最终重载者,它可以省略在运行时执行分派的步骤。如果用户代码希望这样做,还可以禁用动态分派:
struct base {
virtual void foo() const { std::cout << "base" << std::endl; }
void bar() const { std::cout << "bar" << std::endl; }
};
struct derived : base {
virtual void foo() const { std::cout << "derived" << std::endl; }
};
void test( base const & b ) {
b.foo(); // requires runtime dispatch, the type of the referred
// object is unknown at compile time.
b.base::foo();// runtime dispatch manually disabled: output will be "base"
b.bar(); // non-virtual, no runtime dispatch
}
int main() {
derived d;
d.foo(); // the type of the object is known, the compiler can substitute
// the call with d.derived::foo()
test( d );
}
struct base1 {};
struct base2 {
virtual ~base2() {}
};
struct base3 {
protected:
~base3() {}
};
typedef base1 base;
struct derived : base { int x; };
struct other { int y; };
int main() {
std::auto_ptr<derived> d( new derived() ); // ok: deleting at the right level
std::auto_ptr<base> b( new derived() ); // error: deleting through a base
// pointer with non-virtual destructor
}
typedef
更改为base1
,那么析构函数将正确地分派到derived
对象,并且代码不会导致未定义的行为。代价是derived
现在需要一个虚表,每个实例都需要一个指针。更重要的是,derived
不再与other
布局兼容。另一种解决方案是将typedef
更改为base3
,在这种情况下,该问题得以解决,编译器会在那一行报错。缺点是您无法通过对基础类型的指针进行删除操作,优点是编译器可以静态确保不会出现未定义的行为。struct Foo {
void foo() { std::cout << "Foo\n"; }
virtual void virtfoo() { std::cout << "Foo\n"; }
};
struct Bar : public Foo {
void foo() { std::cout << "Bar\n"; }
void virtfoo() { std::cout << "Bar\n"; }
};
int main() {
Bar b;
Foo *pf = &b; // static type of *pf is Foo, dynamic type is Bar
pf->foo(); // MUST print "Foo"
pf->virtfoo(); // MUST print "Bar"
}
Bar
的vtable中,您需要两个不同的插槽来存储Foo :: foo()
和Bar :: foo()
。这意味着即使实现希望这样做,它也是vtable的特例用法。实际上,它不想这样做,这样做没有意义,不要担心。delete
无法编译)。因此,虚拟或受保护的解决了用户意外引发未定义行为的问题。http://www.gotw.ca/publications/mill18.htm
没有用户会创建一个自己的Base<Derived>
对象,除非它是一个Derived
对象,因为这不是CRTP基类的目的。他们只需要能够访问析构函数--所以你可以将其留在公共接口中,或者为了节省一行代码,你可以将其保留为公共的,并依靠用户不做傻事。Derived*
强制转换为Base<Derived>*
,您并不会切换到更通用的接口。除非其他类也将Derived
作为基类,否则没有其他类将会拥有Base<Derived>
作为基类。它不适用于多态基类,因此不要将其作为多态基类。对于你的第一个问题的答案是:不会。只有对虚函数的调用才会在运行时通过虚表进行间接调用。
对于你的第二个问题的答案是:奇异递归模板模式通常使用私有继承来实现。您不需要建立一个“IS-A”关系,因此不需要传递基类指针。
例如,在
template <class Derived> class Base
{
};
class Derived : Base<Derived>
{
};
Base<Derived>*
,然后继续调用delete。因此,你从未尝试通过指向基类的指针删除派生类的对象。因此,析构函数不需要是虚拟的。struct derived : base {}
等同于class derived : public base {}
。但总体论点成立。 - David Rodríguez - dribeasvoid MyFunc(Base* ptr)
,您可以这样使用它:MyFunc(ptr_derived)
。这是传统的建模IS-A关系的方法,当您在MyFunc中调用任何虚函数时,需要进行vtable查找。因此,这是一种建模IS-A关系的模式。template<typename Derived> void MyFunc(Base<Derived> *ptr)
。当您使用MyFunc时,您需要执行MyFunc(ptr_derived);
。编译器将生成与参数类型ptr_derived最匹配的MyFunc()代码副本- MyFunc(Base<Derived> *ptr)
。在MyFunc内部,我们可以假设调用了由接口定义的某个函数,并且指针在编译时静态转换(请查看链接中的impl()函数),没有vtable查找的开销。