使用CRTP在静态多态性中模拟纯虚拟函数是可能的

Is emulating pure virtual function in static polymorphism using CRTP possible?

本文关键字:函数 虚拟 模拟 CRTP 静态 多态性 使用      更新时间:2023-10-16

我正在尝试使用CRTP实现编译时多态性,并希望强制派生类实现该函数。

当前的实现是这样的。

template <class Derived>
struct base {
    void f() {
        static_cast<Derived*>(this)->f();
    }
};
struct derived : base<derived>
{
    void f() {
    ...
    }
};

在这个实现中,如果派生类没有实现f(),那么对函数的调用将陷入无限循环。

如何强制派生类实现类似于纯虚拟函数的函数?我尝试像static_assert(&base::f != &Derived::f, "...")一样使用"static_assert",但它会生成一条错误消息,指出指向不同类的成员函数的两个成员函数指针不可比较。

您可以给覆盖的东西和钩子不同的名称,如下所示:

template <class Derived>
struct base {
    void f() {
        static_cast<Derived*>(this)->fimpl();
    }
    void fimpl() = delete;
};
struct derived : base<derived> {
    void fimpl() { printf("hello worldn"); }
};

这里,fimpl = delete在基中,这样它就不会被意外调用,除非fimpl在派生类中被重写。

您也可以在CRTP中粘贴一个中间隐藏层,以"临时"将f标记为delete:

template <class Derived>
struct base {
    void f() {
        static_cast<Derived*>(this)->f();
    }
};
template <class Derived>
struct intermediate : base<Derived> {
    void f() = delete;
};
struct derived : intermediate<derived> {
    void f() { printf("hello worldn"); }
};
template<typename Derived>
class Base
{
  private:
    static void verify(void (Derived::*)()) {}
  public:
    void f()
    {
        verify(&Derived::f);
        static_cast<Derived*>(this)->f();
    }
};

如果派生类不单独实现f,那么&Derived::f的类型将是void (Base::*)(),这将中断编译。

由于C++11,我们也可以使这个函数通用可变模板。

template<typename Derived>
class Base
{
  private:
    template<typename T, typename...Args>
    static void verify(T (Derived::*)(Args...)) {}
};

这是几年前提出的一个问题,但我最近遇到了这个问题,所以我将把它发布在这里,希望它能帮助一些人。

使用auto作为返回类型可能是另一种解决方案。考虑以下代码:

template<typename Derived>
class Base
{
  public:
    auto f()
    {
        static_cast<Derived*>(this)->f();
    }
};

如果派生类没有提供有效的重载,则此函数将变为递归函数,并且由于auto需要最终返回类型,因此永远无法推导它,因此可以保证会引发编译错误。例如,在MSVC上,它类似于:

a function that returns 'auto' cannot be used before it is defined

这迫使派生类提供实现,就像纯虚拟函数一样。

好处是不需要额外的代码,如果派生类也使用auto作为返回类型,那么这个链可以根据需要运行。在某些情况下,它可以是方便和灵活的,如以下代码中的BaseLevelTwo,它们在调用同一接口f时可以返回不同的类型但是这个链完全禁止从基类直接继承实现,如LevelThree:

template<typename Derived = void>
class Base
{
  public:
    Base() = default;
    ~Base() = default;
    // interface
    auto f()
    {
        return fImpl();
    }
  protected:
    // implementation chain
    auto fImpl()
    {
        if constexpr (std::is_same_v<Derived, void>)
        {
            return int(1);
        }
        else
        {
            static_cast<Derived*>(this)->fImpl();
        }
    }
};
template<typename Derived = void>
class LevelTwo : public Base<LevelTwo>
{
  public:
    LevelTwo() = default;
    ~LevelTwo() = default;
    // inherit interface
    using Base<LevelTwo>::f;
  protected:
    // provide overload
    auto fImpl()
    {
        if constexpr (std::is_same_v<Derived, void>)
        {
            return float(2);
        }
        else
        {
            static_cast<Derived*>(this)->fImpl();
        }
    }
    friend Base;
};
template<typename Derived = void>
class LevelThree : public LevelTwo<LevelThree>
{
  public:
    LevelThree() = default;
    ~LevelThree() = default;
    using LevelTwo<LevelThree>::f;
  protected:
    // doesn't provide new implementation, compilation error here
    using LevelTwo<LevelThree>::fImpl;
    friend LevelTwo;
};

在我的例子中,我处理的派生类也派生自另一个类,该类提供了确定是停止在当前类还是转到派生类所需的额外信息。但在其他情况下,要么使用实际类型而不是"auto"来打破链条,要么使用其他一些技巧。但在这种情况下,虚拟函数可能是的最佳选择。