关于 CRTP 静态多态性的困惑
Confusion about CRTP static polymorphism
我试图围绕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
方法的基类,Dog
、Cat
等覆盖该方法。这是静态多态性:
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
不是类型,第二个不是多态的。
但是,假设您有两个派生类:Derived1
和Derived2
。然后,您可以做的是将func
本身制作为模板。
template <typename T>
void func(Base<T>& x);
然后可以使用派生自Base
的任何类型来调用它,并且它将使用传递的任何参数的静态类型来决定调用哪个函数。
这只是CRTP的用途之一,如果我猜的话,我会说不太常见的一种。你也可以像Nir Friedman在另一个答案中建议的那样使用它,这与静态多态性没有任何关系。
这两种用途在这里都得到了很好的讨论
- 多态性和功能结合
- 找不到成员对象:没有名为get_event()的成员,也处理多态性和向量
- 使用取消引用的指针的多态性会产生意外的结果.为什么?
- C++boost序列化多态性问题
- 如何查找哪个类对象位于数组的特定索引上(多态性)
- 如何在多线程中正确使用unique_ptr进行多态性?
- 具有智能指针的多态性
- 在 C++ 中在堆栈上创建实例时如何保持多态性?
- 继承/多态性 - 我是否被迫使用"protected"变量?
- C++ 多态性在代码::块 17.12 中不起作用
- C++ 泛型和多态性:这种模式可行吗?
- 为什么我们实际上需要运行时多态性?
- 关于 CRTP 静态多态性的困惑
- CRTP 静态多态性:是否可以用模拟替换基类
- 静态多态性策略和CRTP有什么区别
- CRTP的运行时多态性设计和策略
- 使用CRTP在静态多态性中模拟纯虚拟函数是可能的
- 使用CRTP的静态多态性:使用基类调用派生方法
- CRTP和动态多态性编译错误
- C++静态多态性 (CRTP) 并使用派生类中的 typedef