Skip to content

CPlusPlus 实现编译期多态的几种方法

chlict edited this page Apr 15, 2020 · 3 revisions

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++编程,如果想在编译期就依据类型信息决定调用哪些函数,实现编译期多态,总结有如下几种方法。

1. 特例化或部分特例化

特例化或部分特例化(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++元编程常用的手段。

2. CRTP

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函数.

这种方法需要引入继承。

3. 利用函数重载实现函数的编译期多态

函数虽然也支持模板,但并不支持部分特例化。比如想写成这样:

#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编译器,编程实践时也可以添加类似的约束。

注意相比运行期多态,编译期多态虽然节省了运行时开销,但代码体积会变大。