Реализация механизма переопределения виртуальных функций с помощью шаблонов

#c #oop #templates

Вопрос:

Недавно у меня возникла мысль о реализации виртуальных функций без виртуальных таблиц или сохранении указателя с помощью CRTP (хотя static_cast<CRTPamp;>(*this) вместо этого используется.

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

Итак, код таков:

 namespace detail
{
    template<typename T, typename = void>
    struct virtual_set_up
    {
        void operator()(T amp;) {}
    };

    template<typename T>
    struct virtual_set_up<T, std::void_t<decltype(std::declval<T>().set_up())>>
    {
        void operator()(T amp;t) { t.set_up(); }
    };
}

template<typename CRTP>
class base
{
public:
    base() {}

    void set_up() { detail::virtual_set_up<CRTP>()(static_cast<CRTP amp;>(*this)); }

protected:
    ~base() = defau<
};

class settable : public base<settable>
{
public:
    void set_up() { std::cout << "settable: set_up overridden" << std::endl; }
};

class dummy : public base<dummy>
{
public:
};

int main(int, char **)
{
    settable s;
    dummy d;

    base<settable>amp; baseS = s;
    base<dummy>amp; baseD = d;
    baseS.set_up();
    baseD.set_up();

    return 0;
}
 

Однако есть проблема: virtual_set_up<dummy> решает специализацию T с объявленным T::set_up вызовом SEGFAULT при выполнении. Это происходит потому, что dummy публично наследует от базы, у которой действительно есть set_up метод.

Учитывая, что предыдущая проблема разрешима, добавляет ли это какую-либо эффективность по сравнению с обычной виртуальной функцией?

Комментарии:

1. возможно, вас заинтересует работа родителей Шона над полиморфизмом. Это всего лишь первый разговор, который я смог найти youtube.com/watch?v=QGcVXgEVMJg , есть еще

Ответ №1:

Чтобы решить вашу бесконечную рекурсию, вы все равно можете сравнить это « amp;dummy::setup != amp;base<dummy>::setup «:

 namespace detail
{
    template <typename B, typename T, typename = void>
    struct virtual_set_up
    {
        void operator()(Tamp;) {}
    };

    template <typename B, typename T>
    struct virtual_set_up<B, T,
               std::enable_if_t<!std::is_same_v<decltype(amp;B::set_up),
                                                decltype(amp;T::set_up)>>>
    {
        void operator()(Tamp; t) { t.set_up(); }
    };
}

template <typename CRTP>
class base
{
public:
    base() {}

    void set_up() { detail::virtual_set_up<base, CRTP>()(static_cast<CRTP amp;>(*this)); }

protected:
    ~base() = defau<
};
 

ДЕМОНСТРАЦИЯ

Но проще было бы переименовать/разделить его в base<CRTP>

 template <typename CRTP>
class base
{
public:
    base() {}

    void set_up() { static_cast<CRTP amp;>(*this).set_up_v(); }

    void set_up_v() { std::cout << "basen"; }

protected:
    ~base() = defau<
};

class settable : public base<settable>
{
public:
    void set_up_v() { std::cout << "settable: set_up overridden" << std::endl; }
};
 

ДЕМОНСТРАЦИЯ

Добавляет ли это эффективности по сравнению с обычной виртуальной функцией?

Весь код решается при компиляции, нет динамической отправки, поэтому нет затрат на виртуальную отправку…

Но у вас нет ничего полиморфного ни здесь: base<dummy> и base<settable> это несвязанные классы (вам не std::vector<base> нужно хранить их вместе). Так что сравнение несправедливо.

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

Комментарии:

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