CRTP.试着理解一个给定的例子

CRTP. Trying to understand an given example

本文关键字:一个 CRTP      更新时间:2023-10-16

当我试图理解CRTP时,我遇到了这个例子,它对我来说有点模糊。如果我做一些更简单的事情,我可以获得同样的结果:

#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)
}

我说得对吗?

经过思考,我想这个用途是为了允许这样的东西:

template<typename T>
void call(Base<T>& x)
{
    x.method();
}

并称之为

int main()
{
    Derived1 d1;
    Derived2 d2;
    call(d1);
    call(d2);
}

您是对的,这是使用所提供的CRTP示例的一种可能方式。

然而,必须注意的是,正如您已经注意到的那样,示例的main()在给定的情况下显示了一个糟糕的用例(直接调用派生方法),因为我们不需要CRTP或继承方法来使示例工作。


关于此:

vtables真正提供的是使用基类(指针或引用)来调用派生方法。您应该在这里展示如何使用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还是B1::method。相反,因为它是在运行时完成的,所以这个过程往往比事先知道类型要慢。。。

静态多态性

同样的例子以避免使用vtable的方式重新进行:

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*类型执行从A::methodB::method的分辨率时,两种方法之间的主要区别是。CRTP允许编译器了解这个派生方法,因为我们使用模板从基类型交换到派生类型,因此静态多态性。vtable与虚拟函数/方法一起使用,并通过存储指向正确类的方法的函数指针,间接地"执行交换"(这是错误的,但有助于这样想)。

@Etherealone向回答者(真正的单词?)提出的问题是演示我在上面刚刚展示的内容——如何使用CRTP来使用基类指针调用派生类方法,而不是依赖vtable(一层间接实现这一点,而不知道调用代码中的派生类型本身)。

是的,模板函数"call"也可以做同样的工作。但CRTP有时可能会更好。用法,例如:

     Base<Derived1> *d1 = new Derived1;
     d1->method();