Возможна ли эмуляция чистой виртуальной функции в статическом полиморфизме с использованием CRTP?

Я пытаюсь реализовать полиморфизм во время компиляции, используя CRTP, и хочу заставить производный класс реализовать функцию.

Текущая реализация такова.

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

struct derived : base<derived>
{
    void f() {
    ...
    }
};

В этой реализации вызов функции попадает в бесконечный цикл, если производный класс не реализует f(),

Как заставить производный класс реализовать функцию, подобную чисто виртуальной функции? Я пытался использовать "static_assert", как static_assert(&base::f != &Derived::f, "...") но он генерирует сообщение об ошибке, в котором говорится, что два указателя на функцию-член, указывающие на функции-члены разных классов, несопоставимы.

2 ответа

Решение

Вы можете присвоить перезаписываемой вещи разные имена, например:

template <class Derived>
struct base {
    void f() {
        static_cast<Derived*>(this)->fimpl();
    }
    void fimpl() = delete;
};

struct derived : base<derived> {
    void fimpl() { printf("hello world\n"); }
};

Вот, 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 world\n"); }
};
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как возвращаемый тип, эта цепочка может длиться столько, сколько требуется. В некоторых случаях это может быть удобно и гибко, как вBase а также LevelTwo в следующем коде, который может возвращать разные типы при вызове одного и того же интерфейса 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;
};

В моем случае производный класс, над которым я работаю, также является производным от другого класса, который предоставляет дополнительную информацию, необходимую для определения того, останавливаться ли на текущем классе или переходить к производному классу. Но в других случаях можно либо разорвать цепочку, используя фактические типы вместо "авто", либо использовать другие уловки. Но в таких ситуациях, что может быть виртуальной функцией является лучшим выбрал.

Другие вопросы по тегам