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


person Inbae Jeong    schedule 09.02.2015    source источник
comment
Взгляните на ctype::scan_is и ctype::do_scan_is.   -  person user541686    schedule 09.02.2015


Ответы (3)


Вы можете дать объекту, который вы переопределяете, и хуку разные имена, например:

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"); }
};
person tmyklebu    schedule 09.02.2015
comment
Второе решение, ставящее промежуточный класс, прекрасно. - person Inbae Jeong; 09.02.2015
comment
Я бы сделал это первым способом. Второй способ рискует провалиться, если вы забудете наследовать от промежуточного класса. - person tmyklebu; 09.02.2015
comment
Я думаю, это просто проблема с именами. Как насчет того, чтобы поместить реальную базу в пространство имен detail и назвать промежуточный класс base? - person Inbae Jeong; 09.02.2015
comment
Вы можете делать подобные вещи, если хотите. Однако всякий раз, когда вы хотите поговорить о настоящей базе, вы должны называть ее detail::base, и это везде, кроме случаев, когда вы наследуете. Обычно я предпочитаю не усложнять код больше, чем это необходимо, но я также не занимаюсь максимизацией оплачиваемых часов. - person tmyklebu; 09.02.2015
comment
Я понимаю вашу точку зрения, но вы также должны помнить имена функций для интерфейса и реализации, если они различаются. - person Inbae Jeong; 09.02.2015

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...)) {}
};
person jdh8    schedule 30.09.2016

Через этот вопрос задан много лет назад, но я столкнулся с этим недавно, поэтому я просто опубликую его здесь, надеюсь, что это может помочь некоторым людям.

Использование 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;
};

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

person X. Sun    schedule 29.11.2019