关于 CRTP 静态多态性的困惑

Confusion about CRTP static polymorphism

本文关键字:多态性 CRTP 静态 关于      更新时间:2023-10-16

我试图围绕CRTP进行思考。有一些很好的资源,包括这个论坛,但我认为我对静态多态性的基础知识有些困惑。查看以下维基百科条目:

template <class T> 
struct Base
{
void implementation()
{
// ...
static_cast<T*>(this)->implementation();
// ...
}
static void static_func()
{
// ...
T::static_sub_func();
// ...
}
};
struct Derived : public Base<Derived>
{
void implementation();
static void static_sub_func();
};

我知道这有助于我在派生类中拥有不同的 implementation() 变体,有点像编译时虚拟函数。但是,我的困惑是我认为我不能拥有这样的功能

void func(Base x){
x.implementation();
}

就像我对正常的继承和虚函数一样,由于 Base 被模板化,但我必须指定

func(Derived x)

或使用

template<class T> 
func(T x)

那么在这种情况下,CRTP实际上为我买了什么,而不是简单地在Derived::Base中直接影子/实现方法?

struct Base
{
void implementation();
};
struct Derived : public Base
{
void implementation();
static void static_sub_func();
};

问题是,将CRTP描述为"静态多态性"对于CRPT的实际用途并不真正有用或准确。多态性实际上只是具有满足相同接口或协定的不同类型;这些不同类型如何实现该接口与多态性正交。动态多态如下所示:

void foo(Animal& a) { a.make_sound();  } //  could bark, meow, etc

其中Animal是提供虚拟make_sound方法的基类,DogCat等覆盖该方法。这是静态多态性:

template <class T>
void foo(T& a) { a.make_sound(); }

仅此而已。可以在碰巧定义make_sound方法的任何类型上调用foo的静态版本,而无需从基类继承。并且调用将在编译时解析(即您无需为 vtable 调用付费)。

那么CRTP适合在哪里呢?CRTP实际上根本不是关于接口的,所以它与多态性无关。CRTP 是关于让您更轻松地实现事物。CRTP 的神奇之处在于,它可以将内容直接注入到类型的接口中,并完全了解派生类型提供的所有内容。一个简单的例子可能是:

template <class T>
struct MakeDouble {
T double() {
auto& me = static_cast<T&>(*this);
return me + me;
};

现在,任何定义加法运算符的类也可以被赋予一个double方法:

class Matrix : MakeDouble<Matrix> ...
Matrix m;
auto m2 = m.double();

CRTP是关于帮助实施,而不是接口。因此,不要太纠结于它通常被称为"静态多态性"的事实。如果你想要一个关于CRTP可以用来做什么的真实规范的例子,请考虑Andrei Alexandrescu的现代C++设计的第一章。不过,慢慢来:-)。

只有当涉及多个函数时,CRTP 的优势才会变得明显。考虑以下代码(没有 CRTP):

struct Base
{
int algorithm(int x)
{
prologue();
if (x > 42)
x = downsize(x);
x = crunch(x);
epilogue();
return x;
}
void prologue()
{}
int downsize(int x)
{ return x % 42; }
int crunch(int x)
{ return -x; }
void epilogue()
{}
};
struct Derived : Base
{
int downsize(int x)
{
while (x > 42) x /= 2;
return x;
}
void epilogue()
{ std::cout << "We're done!n"; }
};
int main()
{
Derived d;
std::cout << d.algorithm(420);
}

这输出:

0

[现场示例]

由于C++类型系统的静态性质,对d.algorithm的调用调用Base的所有函数。不会调用Derived中尝试的覆盖。

使用 CRTP 时,这种情况会发生变化:

template <class Self>
struct Base
{
Self& self() { return static_cast<Self&>(*this); }
int algorithm(int x)
{
self().prologue();
if (x > 42)
x = self().downsize(x);
x = self().crunch(x);
self().epilogue();
return x;
}
void prologue()
{}
int downsize(int x)
{ return x % 42; }
int crunch(int x)
{ return -x; }
void epilogue()
{}
};
struct Derived : Base<Derived>
{
int downsize(int x)
{
while (x > 42) x /= 2;
return x;
}
void epilogue()
{ std::cout << "We're done!n"; }
};
int main()
{
Derived d;
std::cout << d.algorithm(420);
}

输出:

大功告成!
-26

[现场示例]

这样,只要Derived提供"覆盖",Base中的实现实际上就会调用Derived

这甚至可以在您的原始代码中可见:如果Base不是 CRTP 类,则它对static_sub_func的调用永远不会解析为Derived::static_sub_func


至于CRTP与其他方法相比的优势是什么:

  • CRTP 与virtual函数:

    CRTP 是一个编译时构造,这意味着没有关联的运行时开销。通过基类引用调用虚函数(通常)需要通过指向函数的指针进行调用,因此会产生间接成本并防止内联。

  • CRTP 与简单地在Derived中实现所有内容:

    基类代码重用。

当然,CRTP 是一个纯粹的编译时构造。要实现它允许的编译时多态性,您必须使用编译时多态构造:模板。有两种方法可以执行此操作:

template <class T>
int foo(Base<T> &actor)
{
return actor.algorithm(314);
}
template <class T>
int bar(T &actor)
{
return actor.algorithm(314);
}

前者更接近运行时多态性并提供更好的类型安全性,后者更基于鸭子类型。

你是对的,两者都不是

void func(Base x);

void func(Derived x);

给你静态多态性。第一个不编译,因为Base不是类型,第二个不是多态的。

但是,假设您有两个派生类:Derived1Derived2。然后,您可以做的是将func本身制作为模板。

template <typename T>
void func(Base<T>& x);

然后可以使用派生自Base的任何类型来调用它,并且它将使用传递的任何参数的静态类型来决定调用哪个函数。


这只是CRTP的用途之一,如果我猜的话,我会说不太常见的一种。你也可以像Nir Friedman在另一个答案中建议的那样使用它,这与静态多态性没有任何关系。

这两种用途在这里都得到了很好的讨论