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

时间:2016-08-01 01:47:42

标签: c++ c++11 crtp

当我试图理解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)
}

我说错了吗?

2 个答案:

答案 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::methodB::method的分辨率。 CRTP允许编译器知道这个派生方法,因为我们使用模板从基类交换到派生类型 - 因此静态多态。 A*与虚函数/方法一起使用,通过将函数指针存储到正确的类方法,间接地“执行交换”(错误但有助于这样思考)。

@Etherealone要求回答者(真正的单词?)是为了演示我刚刚在上面展示的内容 - 如何使用CRTP来使用基类指针来调用派生类方法,而不是依赖于vtable(实现相同的间接层,不知道调用代码中的派生类型本身)。

答案 1 :(得分:0)

是的,模板功能“call”可以完成同样的工作。但CRTP有时可能会更好。 用法例如:

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