当我试图理解CRTP时,我偶然发现了example,这对我来说有点模糊。如果我做一些像这样简单的事情,我可以获得相同的结果:
#pragma once
#include <iostream>
template <typename T>
class Base
{
public:
void method() {
static_cast<T*>(this)->method();
}
};
class Derived1 // : public Base<Derived1> <-- commented inherintance
{
public:
void method() {
std::cout << "Derived1 method" << std::endl;
}
};
class Derived2 // : public Base<Derived2> <-- commmented inherintance
{
public:
void method() {
std::cout << "Derived2 method" << std::endl;
}
};
#include "crtp.h"
int main()
{
Derived1 d1;
Derived2 d2;
d1.method();
d2.method();
return 0;
}
我的问题是:CRTP的目的是什么?经过一番思考后,我猜这个用法是允许这样的:
template<typename T>
void call(Base<T>& x)
{
x.method();
}
并像这样称呼它
int main()
{
Derived1 d1;
Derived2 d2;
call(d1);
call(d2)
}
我说错了吗?
答案 0 :(得分:1)
经过深思熟虑后我想这个用法是允许这样的:
template<typename T> void call(Base<T>& x) { x.method(); }
并像这样称呼它
int main() { Derived1 d1; Derived2 d2; call(d1); call(d2); }
您是对的,这是使用提供的CRTP示例的一种可能方式。
但是必须注意 - 正如你所知 - 示例的main()
显示了一个糟糕的用例(直接调用派生方法),因为我们不需要CRTP或示例的继承方法工作。
关于这个:
vtable真正提供的是使用基类(指针或引用)来调用派生方法。你应该在这里展示如何使用CRTP。
要理解这一点,谨慎地解释为什么我们首先需要多态性。在必须理解的任何其他事情之前,或CRTP看起来只不过是模板的巧妙技巧。
您最初的猜测是现实 - 当您想要访问行为/派生类的方法时,您会想要多态,这会覆盖(或定义)我们需要通过基类提供的行为
在这样的普通继承中:
struct A { void method() {} };
struct B : A { void method() {} };
int main () {
B b;
b.method();
return 0;
}
B::method()
会被触发,是的,但是如果我们省略B::method()
那么它实际上会调用A::method()
。从这个意义上讲,B::method()
会覆盖 A::method()
。
如果我们想要调用B::method()
,但只有一个A
对象,那就不够了:
int main () {
B b;
A *a = &b;
a->method(); // calls A::method();
return 0;
}
有两种方法可以实现这一目标(至少):动态多态或静态多态。
struct A1 {
virtual ~A1(){}
virtual void method() {}
};
struct B1 : A1 {
virtual ~B1(){}
virtual void method() override {}
};
int main () {
B1 b;
A1 *a = &b;
a->method(); // calls B1::method() but this is not known until runtime.
return 0;
}
详细说明,因为A1
具有虚方法,所以此类存在一个特殊表(vtable
),其中为每个虚方法存储函数指针。派生类(在某种意义上)可以覆盖此函数指针,这是B1
的每个实例所做的 - 设置为B1::method
。
因为此信息在运行时存在,所以编译器不需要知道查找A1::method
的函数指针并调用它指向的任何内容,无论它是A1::method
还是{{ 1}}。相反,因为它是在运行时完成的,所以该过程往往比事先知道类型要慢......
以避免使用vtable的方式重新完成相同的示例:
B1::method
这次我们没有使用任何动态查找,根据您的示例函数template < class T > struct A2 {
void method() {
T *derived_this = static_cast<T*>(this);
derived_this->method();
}
};
struct B2 : A2 < B2 > {
void method() {}
};
int main () {
B2 b;
A2<B2> *a = &b; // typically seen as a templated function argument
a->method(); // calls B2::method() statically, known at compile-time
return 0;
}
>
两种方法之间的主要区别是 时执行使用template<class T> call(Base<T>*);
类型的A::method
到B::method
的分辨率。 CRTP允许编译器知道这个派生方法,因为我们使用模板从基类交换到派生类型 - 因此静态多态。 A*
与虚函数/方法一起使用,通过将函数指针存储到正确的类方法,间接地“执行交换”(错误但有助于这样思考)。
@Etherealone要求回答者(真正的单词?)是为了演示我刚刚在上面展示的内容 - 如何使用CRTP来使用基类指针来调用派生类方法,而不是依赖于vtable
(实现相同的间接层,不知道调用代码中的派生类型本身)。
答案 1 :(得分:0)
是的,模板功能“call”可以完成同样的工作。但CRTP有时可能会更好。 用法例如:
Base<Derived1> *d1 = new Derived1;
d1->method();