避免动态多态性

如何在 C + + 中使用 CRTP 来避免虚拟成员函数的开销?

28383 次浏览

I had to look up CRTP. Having done that, however, I found some stuff about Static Polymorphism. I suspect that this is the answer to your question.

It turns out that ATL uses this pattern quite extensively.

This Wikipedia answer has all you need. Namely:

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();
};

Although I don't know how much this actually buys you. The overhead of a virtual function call is (compiler dependent, of course):

  • Memory: One function pointer per virtual function
  • Runtime: One function pointer call

While the overhead of CRTP static polymorphism is:

  • Memory: Duplication of Base per template instantiation
  • Runtime: One function pointer call + whatever static_cast is doing

I've been looking for decent discussions of CRTP myself. Todd Veldhuizen's Techniques for Scientific C++ is a great resource for this (1.3) and many other advanced techniques like expression templates.

Also, I found that you could read most of Coplien's original C++ Gems article at Google books. Maybe that's still the case.

There are two ways.

The first one is by specifying the interface statically for the structure of types:

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.
};

The second one is by avoiding the use of the reference-to-base or pointer-to-base idiom and do the wiring at compile-time. Using the above definition, you can have template functions that look like these:

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

So combining the structure/interface definition and the compile-time type deduction in your functions allows you to do static dispatch instead of dynamic dispatch. This is the essence of static polymorphism.

CRTP/SFINAE Static Dispatching with Strict Signature Checking

This solution for static dispatching uses CRTP and SFINAE, which is not new. What is unique about this solution is that it also enforces strict signature checking, which allows us to statically dispatch overloaded methods in the same way dynamic dispatching works for virtual functions.

To begin, let's first look at the limitations of a traditional solution using SFINAE. The following was taken from Ben Deane's CppCon 2016 Lightning Talk “A Static Alternative to Virtual Functions, Using Expression 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()))

Using the above code, the template instantiation has_complete<DerivedClass> will, in general, do what you would expect. If DerivedClass has a method named Complete that accepts a std::string, the resulting type will be std::true_type.

What happens when you want to overload a function?

template <class Derived>
struct Base {
std::string foo(bool);
std::string foo(int);
...
};


struct Derived : public Base<Derived>
{
std::string foo(int);
};

In this case, Derived does, in fact, have a method named foo that accepts a bool because bool is implicitly convertible to int. Therefore, even if we only set up dispatching for the signature that accepts a bool, has_foo<Derived> would resolve to std::true_type, and the call would be dispatched to Derived::foo(int). Is this what we want? Probably not, because this is not the way that virtual functions work. A function can only override a virtual function if the two signatures match exactly. I propose that we make a static dispatch mechanism that behaves in the same way.

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;

That's nice, but that alone doesn't enforce signature checks. To perform strict signature checking, we have to properly define the template template parameter Op. To do this, we will make use of a std::integral_constant of a member function pointer. Here's what that looks like:

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>

Defining our Ops in this way allows us to dispatch only to methods with an exact signature match.

// 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>;

Now let's put it all together.

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

Writing a macro that creates a dispatcher for a non-overloaded member function would be simple enough, but making one that supports overloaded functions would be a bit more challenging. If anybody cares to contribute that, I'd welcome the addition.