如何在 C++ 中使用 CRTP 来避免虚拟成员函数的开销?
5 回答
有两种方法。
第一个是通过为类型结构静态指定接口:
template <class Derived>
struct base {
void foo() {
static_cast<Derived *>(this)->foo();
};
};
struct my_type : base<my_type> {
void foo(); // required to compile.
};
struct your_type : base<your_type> {
void foo(); // required to compile.
};
第二个是避免使用对基址的引用或指向基址的指针,并在编译时进行布线。使用上面的定义,您可以拥有如下所示的模板函数:
template <class T> // T is deduced at compile-time
void bar(base<T> & obj) {
obj.foo(); // will do static dispatch
}
struct not_derived_from_base { }; // notice, not derived from base
// ...
my_type my_instance;
your_type your_instance;
not_derived_from_base invalid_instance;
bar(my_instance); // will call my_instance.foo()
bar(your_instance); // will call your_instance.foo()
bar(invalid_instance); // compile error, cannot deduce correct overload
因此,结合函数中的结构/接口定义和编译时类型推导,您可以进行静态分派而不是动态分派。这就是静态多态的本质。
我自己一直在寻找关于 CRTP 的体面讨论。Todd Veldhuizen 的 Technicals for Scientific C++是此 (1.3) 和许多其他高级技术(如表达式模板)的重要资源。
此外,我发现您可以在 Google 书籍中阅读 Coplien 的大部分原始 C++ Gems 文章。也许情况仍然如此。
我不得不查找CRTP。然而,在这样做之后,我发现了一些关于Static Polymorphism的东西。我怀疑这是您问题的答案。
事实证明,ATL相当广泛地使用了这种模式。
具有严格签名检查的 CRTP/SFINAE 静态调度
这种静态调度的解决方案使用了 CRTP 和 SFINAE,这并不新鲜。这个解决方案的独特之处在于它还强制执行严格的签名检查,这允许我们以与动态分派对虚函数相同的方式静态分派重载方法。
首先,让我们先看看使用 SFINAE 的传统解决方案的局限性。以下内容摘自 Ben Deane 的 CppCon 2016 Lightning Talk “虚拟函数的静态替代方案,使用表达式 SFINAE”。
#define SFINAE_DETECT(name, expr) \
template <typename T> \
using name##_t = decltype(expr); \
template <typename T, typename = void> \
struct has_##name : public std::false_type {}; \
template <typename T> \
struct has_##name<T, void_t<name##_t<T>>> : public std::true_type {};
// detect CommonPrefix(string)
SFINAE_DETECT(common_prefix,
declval<T>().CommonPrefix(std::string()))
使用上面的代码,模板实例化has_complete<DerivedClass>
通常会按照您的预期进行。如果DerivedClass
有一个名为
Complete
接受 a的方法std::string
,则结果类型将为
std::true_type
.
当你想重载一个函数时会发生什么?
template <class Derived>
struct Base {
std::string foo(bool);
std::string foo(int);
...
};
struct Derived : public Base<Derived>
{
std::string foo(int);
};
在这种情况下,Derived
确实有一个名为的方法foo
,它接受 a
bool
因为bool
可以隐式转换为int
. 因此,即使我们只为接受 bool 的签名设置调度,has_foo<Derived>
也会解析为std::true_type
,并且调用将被调度到Derived::foo(int)
。这是我们想要的吗?可能不会,因为这不是虚函数的工作方式。如果两个签名完全匹配,一个函数只能覆盖一个虚函数。我建议我们创建一个以相同方式运行的静态调度机制。
template <template <class...> class Op, class... Types>
struct dispatcher;
template <template <class...> class Op, class T>
struct dispatcher<Op, T> : std::experimental::detected_t<Op, T> {};
template <template <class...> class Op, class T, class... Types>
struct dispatcher<Op, T, Types...>
: std::experimental::detected_or_t<
typename dispatcher<Op, Types...>::type, Op, T> {};
template <template <class...> class Op, class... Types>
using dispatcher_t = typename dispatcher<Op, Types...>::type;
这很好,但仅此一项并不能强制执行签名检查。要执行严格的签名检查,我们必须正确定义模板模板参数
Op
。为此,我们将使用std::integral_constant
一个成员函数指针。看起来是这样的:
template <class T>
using foo_op_b = std::integral_constant<std::string(T::*)(bool), &T::foo>;
template <class T>
using foo_op_i = std::integral_constant<std::string(T::*)(int), &T::foo>
以这种方式定义我们Op
的 s 允许我们仅调度具有完全签名匹配的方法。
// Resolves to std::integral_constant<std::string(T::*)(bool), &Derived::foo>
using foo_bool_ic = dispatcher_t<foo_op_b, Derived, Defaults>;
// Resolves to std::integral_constant<std::string(T::*)(int), &Defaults::foo>
using foo_int_ic = dispatcher_t<foo_op_i, Derived, Defaults>;
现在让我们把它们放在一起。
#include <iostream>
#include <experimental/type_traits>
#include <string>
template <template <class...> class Op, class... Types>
struct dispatcher;
template <template <class...> class Op, class T>
struct dispatcher<Op, T> : std::experimental::detected_t<Op, T> {};
template <template <class...> class Op, class T, class... Types>
struct dispatcher<Op, T, Types...>
: std::experimental::detected_or_t<
typename dispatcher<Op, Types...>::type, Op, T> {};
template <template <class...> class Op, class... Types>
using dispatcher_t = typename dispatcher<Op, Types...>::type;
// Used to deduce class type from a member function pointer
template <class R, class T, class... Args>
auto method_cls(R(T::*)(Args...)) -> T;
struct Defaults {
std::string foo(bool value) { return value ? "true" : "false"; }
std::string foo(int value) { return value ? "true" : "false"; }
// Ensure that the class is polymorphic so we can use dynamic_cast
virtual ~Defaults() {};
};
template <class Derived>
struct Base : Defaults {
template <class T>
using foo_op_b = std::integral_constant<std::string(T::*)(bool), &T::foo>;
template <class T>
using foo_op_i = std::integral_constant<std::string(T::*)(int), &T::foo>;
std::string foo(bool value) {
auto method = dispatcher_t<foo_op_b, Derived, Defaults>::value;
auto *target = dynamic_cast<decltype(method_cls(method)) *>(this);
return (target->*method)(value);
}
std::string foo(int value) {
auto method = dispatcher_t<foo_op_i, Derived, Defaults>::value;
auto *target = dynamic_cast<decltype(method_cls(method)) *>(this);
return (target->*method)(value);
}
};
struct Derived : Base<Derived> {
std::string foo(bool value) { return value ? "TRUE" : "FALSE"; }
};
int main() {
Derived d;
std::cout << dynamic_cast<Base<Derived> *>(&d)->foo(true) << std::endl; // TRUE
std::cout << dynamic_cast<Base<Derived> *>(&d)->foo(1) << std::endl; // true
}
编写一个为非重载成员函数创建分派器的宏很简单,但创建一个支持重载函数的宏会更具挑战性。如果有人愿意贡献,我会欢迎添加。
这个维基百科的答案有你需要的一切。即:
template <class Derived> struct Base
{
void interface()
{
// ...
static_cast<Derived*>(this)->implementation();
// ...
}
static void static_func()
{
// ...
Derived::static_sub_func();
// ...
}
};
struct Derived : Base<Derived>
{
void implementation();
static void static_sub_func();
};
虽然我不知道这实际上给你买了多少。虚函数调用的开销是(当然取决于编译器):
- 内存:每个虚函数一个函数指针
- 运行时:一次函数指针调用
而 CRTP 静态多态的开销是:
- 内存:每个模板实例化的 Base 副本
- 运行时:一个函数指针调用 + static_cast 正在做的任何事情