-
Notifications
You must be signed in to change notification settings - Fork 0
CPlusPlus 实现编译期多态的几种方法
c++的多态可以有运行期多态和编译期多态(static polymorphism)。运行期多态一般通过继承和虚函数来实现,比如:
class Base {
public:
virtual void foo() {
std::cout << "Base::foo()" << std::endl;
}
};
class Derived : public Base {
public:
void foo() override {
std::cout << "Derived::foo()" << std::endl;
}
};
void Dispatch(Base &p) {
p.foo();
}
运行期多态是通过虚函数表来实现动态分发,因此有一定的开销。
采用现代c++编程,如果想在编译期就依据类型信息决定调用哪些函数,实现编译期多态,总结有如下几种方法。
特例化或部分特例化(partial specialization)是最容易想到的方法。对不同的模板参数做不同的实现,编译器就完成了编译期的选择。
#include <iostream>
struct Rect;
struct Circle;
template <typename Tag>
struct Drawer {
template <typename View>
void draw(View &&view) const = delete;
};
template <>
struct Drawer<Rect> {
template <typename View>
void draw(View &&view) const {
std::cout << "Draw " << view << std::endl;
}
};
template <>
struct Drawer<Circle> {
template <typename View>
void draw(View &&view) const {
std::cout << "Draw " << view << std::endl;
}
};
template <typename Drawer, typename View>
void Dispatch(View &&view, Drawer &&drawer) {
drawer.draw(view);
}
template <typename Tag>
Drawer<Tag> drawer{};
int main() {
Dispatch("rect", drawer<Rect>);
Dispatch("circle", drawer<Circle>);
}
该例中定义了一个模板化的Drawer,用不同的类型特例化这个模板,得到Drawer<Rect>
和Drawer<Circle>
,它们拥有不同的draw()实现。在dispatch的时候,根据Drawer类型的不同,分别调用不同的实现版本。
template <typename Tag> Drawer<Tag> drawer{};
定义了一个模板变量。如果不定义这个变量,则调用Dispatch时需构造临时的Drawer()对象,多写一对括号:
Dispatch("rect", Drawer<Rect>());
Dispatch("circle", Drawer<Circle>());
如果要实施多态的接口类里只有一个抽象函数,即Single Abstract Method(SAM),可以重载operator ()来实现。如上例,可以写成
template <>
struct Drawer<Rect> {
template <typename View>
void operator()(View &&view) const {
std::cout << "Draw " << view << std::endl;
}
};
这样在调用点不必再写drawer.draw(view),而是写成drawer(view)就可以了。 这种方法需要额外定义tag,略显多余。不过tag也是c++元编程常用的手段。
CRTP(Curiously Recurring Template Pattern)定义一个模板基类,子类继承这个基类时将子类本身的类型作为模板参数,实例化基类的类型。基类里由于有了子类的类型作形参,因此可以把自己static_cast成子类,进而调用子类的方法。该技巧可用来实现编译期多态:
#include <iostream>
template <typename T>
struct Base {
template <typename View>
void draw(View &&view) const {
if constexpr (T::drawable) {
const T *derived = static_cast<const T *>(this);
derived->draw(view);
} else {
this->draw_default(view);
}
}
template <typename View>
void draw_default(View &&view) const {
std::cout << "Base::draw()" << std::endl;
}
};
struct Derived1 : Base<Derived1> {
static constexpr bool drawable = true;
template <typename View>
void draw(View &&view) const {
std::cout << "Derived1::draw(view)" << std::endl;
}
};
struct Derived2 : Base<Derived2> {
static constexpr bool drawable = false;
};
template <typename T, typename View>
auto Dispatch(View &&view, Base<T> &&drawer) {
drawer.draw(view);
}
int main() {
auto view = "view";
Dispatch(view, Derived1());
Dispatch(view, Derived2());
}
本例中增加了几个小功能:draw函数是个模板函数,带有类型参数View;判断子类型是否drawable。除此以外跟运行时多态的实现方法差不多。最关键的是Base类中将this强转成T*,然后在编译期就决定调用哪个子类型的draw函数.
这种方法需要引入继承。
函数虽然也支持模板,但并不支持部分特例化。比如想写成这样:
#include <iostream>
struct Rect;
template <typename Drawer, typename View>
auto draw(View &&view, Drawer &&drawer) {
std::cout << "Default draw" << std::endl;
}
template <typename View>
auto draw<Rect>(View &&view) {
std::cout << "Draw rect" << std::endl;
}
编译器会报如下错误:
error: function template partial specialization is not allowed
如果想对模板函数实施编译期多态,可以用函数重载的方式解决:
#include <iostream>
struct RectDrawer{};
struct CircleDrawer{};
template <typename View>
auto draw(View &&view, RectDrawer dummy) {
std::cout << "Draw rect" << std::endl;
}
template <typename View>
auto draw(View &&view, CircleDrawer dummy) {
std::cout << "Draw circle" << std::endl;
}
int main() {
draw("rect", RectDrawer());
draw("circle", CircleDrawer());
}
两个draw函数通过最后一个参数的类型不同加以区分,例子中也没有使用dispatcher。如果要扩充Drawer,只需要增加一个draw函数,但是感觉封装并不是很好,缺少抽象。
本文介绍了三种编译期多态的实现方法。特例化或部分特例化相对容易扩展,扩展时只需要增加一个tag以及对应的特例化实现(类似于添加一个子类以及子类特有的实现)。CRTP更接近于运行时多态的风格,扩展也容易,只是要引入继承,不像主流c++元编程的方法。函数的编译期多态借助dummy参数来解决,相比前两种方法,封装性略显不足。
本文介绍的方法都没有对实施多态的接口添加必要的约束,比如特例化方法,Drawer的各个特例(Drawer<Rect>
和Drawer<Circle>
)应满足Drawable接口,保证调用点处可以安全的调用draw()函数。借助c++20的concept可以改进这一点。即便没有c++20编译器,编程实践时也可以添加类似的约束。
注意相比运行期多态,编译期多态虽然节省了运行时开销,但代码体积会变大。