CRTP, чтобы избежать динамического полиморфизма

Как я могу использовать CRTP в C++, чтобы избежать накладных расходов на виртуальные функции-члены?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
90
0
26 937
5

Ответы 5

Пришлось искать CRTP. Однако, сделав это, я нашел кое-что о Статический полиморфизм. Подозреваю, что это ответ на ваш вопрос.

Оказывается, ATL довольно широко использует этот шаблон.

Этот В Википедии есть все, что вам нужно. А именно:

template <class Derived> struct Base
{
    void interface()
    {
        // ...
        static_cast<Derived*>(this)->implementation();
        // ...
    }

    static void static_func()
    {
        // ...
        Derived::static_sub_func();
        // ...
    }
};

struct Derived : Base<Derived>
{
    void implementation();
    static void static_sub_func();
};

Хотя я не знаю, сколько это на самом деле тебе дает. Накладные расходы на вызов виртуальной функции (конечно, зависят от компилятора):

  • Память: один указатель на каждую виртуальную функцию
  • Время выполнения: один вызов указателя функции

В то время как накладные расходы статического полиморфизма CRTP составляют:

  • Память: дублирование базы на создание экземпляра шаблона
  • Время выполнения: один вызов указателя на функцию + все, что делает static_cast

Фактически, дублирование Base для экземпляра шаблона является иллюзией, потому что (если у вас еще нет vtable) компилятор объединит хранилище базы и производной в единую структуру для вас. Вызов указателя функции также оптимизируется компилятором (часть static_cast).

Dean Michael 04.11.2008 21:52

Кстати, ваш анализ CRTP неверен. Должно быть: Память: Ничего, как сказал Дин Майкл. Время выполнения: один (более быстрый) вызов статической функции, а не виртуальной, и в этом весь смысл упражнения. static_cast ничего не делает, он просто позволяет скомпилировать код.

Frederik Slijkerman 05.11.2008 11:26

Я хочу сказать, что базовый код будет дублироваться во всех экземплярах шаблона (то самое слияние, о котором вы говорите). Это похоже на наличие в шаблоне только одного метода, который полагается на параметр шаблона; все остальное лучше в базовом классе, иначе он втягивается («объединяется») несколько раз.

user23167 05.11.2008 19:37

Каждый метод в базе будет снова скомпилирован для каждого производного. В (ожидаемом) случае, когда каждый экземпляр метода отличается (из-за того, что свойства Derived различны), это не обязательно может считаться накладными расходами. Но это может привести к увеличению общего размера кода по сравнению с ситуацией, когда сложный метод в (нормальном) базовом классе вызывает виртуальные методы подклассов. Кроме того, если вы поместите служебные методы в Base <Derived>, которые на самом деле вообще не зависят от <Derived>, они все равно будут созданы. Возможно, глобальная оптимизация несколько это исправит.

greggo 01.11.2016 20:39

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

John P 22.10.2017 22:15

Я сам искал достойные обсуждения CRTP. Методы научного C++ Тодда Велдхуизена - отличный ресурс для этого (1.3) и многих других продвинутых методов, таких как шаблоны выражений.

Кроме того, я обнаружил, что вы можете прочитать большую часть оригинальной статьи Коплиена о C++ Gems в книгах Google. Может быть, это все еще так.

@fizzer Я прочитал предложенную вами часть, но все еще не понимаю, что означает двойная сумма шаблона <class T_leaftype> (Matrix <T_leaftype> & A); покупает вас по сравнению с шаблоном <class Whatever> с двойной суммой (Whatever & A);

Anton Daneyko 16.04.2013 21:45

@AntonDaneyko При вызове базового экземпляра вызывается сумма базового класса, например «площадь фигуры» с реализацией по умолчанию, как если бы это был квадрат. Цель CRTP в этом случае состоит в том, чтобы разрешить наиболее производную реализацию, «область трапеции» и т. д., Сохраняя при этом возможность ссылаться на трапецию как на форму до тех пор, пока не потребуется производное поведение. По сути, всякий раз, когда вам обычно понадобится dynamic_cast или виртуальные методы.

John P 22.10.2017 21:44

Есть два пути.

Первый заключается в статическом указании интерфейса для структуры типов:

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

struct my_type : base<my_type> {
  void foo(); // required to compile.
};

struct your_type : base<your_type> {
  void foo(); // required to compile.
};

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

template <class T> // T is deduced at compile-time
void bar(base<T> & obj) {
  obj.foo(); // will do static dispatch
}

struct not_derived_from_base { }; // notice, not derived from base

// ...
my_type my_instance;
your_type your_instance;
not_derived_from_base invalid_instance;
bar(my_instance); // will call my_instance.foo()
bar(your_instance); // will call your_instance.foo()
bar(invalid_instance); // compile error, cannot deduce correct overload

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

Я хотел бы подчеркнуть, что not_derived_from_base не является производным от base и не является производным от base ...

leftaroundabout 24.02.2012 16:27

На самом деле объявление foo () внутри my_type / your_type не требуется. codepad.org/ylpEm1up (вызывает переполнение стека) - есть ли способ обеспечить определение foo во время компиляции? - Хорошо, нашел решение: ideone.com/C6Oz9 - Возможно, вы захотите исправить это в своем ответе.

cooky451 03.03.2012 22:35

Не могли бы вы объяснить мне, какова мотивация использования CRTP в этом примере? Если бы bar был определен как template <class T> void bar (T & obj) {obj.foo (); }, тогда подойдет любой класс, предоставляющий foo. Итак, исходя из вашего примера, похоже, что единственное использование CRTP - это указать интерфейс во время компиляции. Это то, для чего это нужно?

Anton Daneyko 16.04.2013 21:27

@Dean Michael Действительно, код в примере компилируется, даже если foo не определено в my_type и your_type. Без этих переопределений рекурсивно вызывается base :: foo (и stackoverflows). Так что, может быть, вы хотите исправить свой ответ, как показал cooky451?

Anton Daneyko 16.04.2013 21:37

@mezhaka: Да, пример Дина Майкла неполный, потому что, как вы показываете, его можно было бы более кратко реализовать без CRTP. Но добавьте template<class T> bar(base2<T> &obj) { obj.quux(); } - то есть второй базовый класс с другой реализацией bar() - и полезность CRTP станет очевидной.

Nemo 18.09.2014 21:36

Статическая рассылка CRTP / SFINAE со строгой проверкой подписи

Это решение для статической диспетчеризации использует CRTP и SFINAE, что не ново. Уникальность этого решения в том, что оно также требует строгой подписи проверка, которая позволяет нам статически отправлять перегруженные методы в один и тот же способ динамической диспетчеризации работает для виртуальных функций.

Для начала давайте сначала посмотрим на ограничения традиционного решения, использующего СФИНАЕ. Следующее было взято из выступления Бена Дина на конференции CppCon 2016 Lightning Talk. «Статическая альтернатива виртуальным функциям, использующая выражение SFINAE».

#define SFINAE_DETECT(name, expr)                                       \
  template <typename T>                                                 \
  using name##_t = decltype(expr);                                      \
  template <typename T, typename = void>                                \
  struct has_##name : public std::false_type {};                        \
  template <typename T>                                                 \
  struct has_##name<T, void_t<name##_t<T>>> : public std::true_type {};

// detect CommonPrefix(string)
SFINAE_DETECT(common_prefix,
              declval<T>().CommonPrefix(std::string()))

Используя приведенный выше код, создание экземпляра шаблона has_complete<DerivedClass> будет, в общем, делать то, что вы ожидаете. Если DerivedClass имеет метод с именем Complete, который принимает std::string, результирующий тип будет std::true_type.

Что происходит, когда вы хотите перегрузить функцию?

template <class Derived>
struct Base {
    std::string foo(bool);
    std::string foo(int);
    ...
};

struct Derived : public Base<Derived>
{
    std::string foo(int);
};

В этом случае у Derived действительно есть метод с именем foo, который принимает bool, потому что bool неявно конвертируется в int. Следовательно, даже если мы настроим отправку только для подписи, которая принимает логическое значение, has_foo<Derived> будет преобразован в std::true_type, и вызов будет отправлено на Derived::foo(int). Мы этого хотим? Наверное, нет, потому что виртуальные функции работают не так. Функция может только переопределить виртуальная функция, если две подписи точно совпадают. Предлагаю провести статический механизм диспетчеризации, который ведет себя таким же образом.

template <template <class...> class Op, class... Types>
struct dispatcher;

template <template <class...> class Op, class T>
struct dispatcher<Op, T> : std::experimental::detected_t<Op, T> {};

template <template <class...> class Op, class T, class... Types>
struct dispatcher<Op, T, Types...>
  : std::experimental::detected_or_t<
    typename dispatcher<Op, Types...>::type, Op, T> {};

template <template <class...> class Op, class... Types>
using dispatcher_t = typename dispatcher<Op, Types...>::type;

Это хорошо, но одно это не требует проверки подписи. Выполнить строгий проверка подписи, мы должны правильно определить параметр шаблона шаблона Op. Для этого мы воспользуемся std::integral_constant члена указатель на функцию. Вот как это выглядит:

template <class T>
using foo_op_b = std::integral_constant<std::string(T::*)(bool), &T::foo>;

template <class T>
using foo_op_i = std::integral_constant<std::string(T::*)(int), &T::foo>

Определение наших Op таким образом позволяет нам отправлять только методы с точное совпадение подписи.

// Resolves to std::integral_constant<std::string(T::*)(bool), &Derived::foo>
using foo_bool_ic = dispatcher_t<foo_op_b, Derived, Defaults>;

// Resolves to std::integral_constant<std::string(T::*)(int), &Defaults::foo>
using foo_int_ic = dispatcher_t<foo_op_i, Derived, Defaults>;

А теперь давайте все вместе.

#include <iostream>
#include <experimental/type_traits>
#include <string>

template <template <class...> class Op, class... Types>
struct dispatcher;

template <template <class...> class Op, class T>
struct dispatcher<Op, T> : std::experimental::detected_t<Op, T> {};

template <template <class...> class Op, class T, class... Types>
struct dispatcher<Op, T, Types...>
  : std::experimental::detected_or_t<
    typename dispatcher<Op, Types...>::type, Op, T> {};

template <template <class...> class Op, class... Types>
using dispatcher_t = typename dispatcher<Op, Types...>::type;


// Used to deduce class type from a member function pointer
template <class R, class T, class... Args>
auto method_cls(R(T::*)(Args...)) -> T;


struct Defaults {
    std::string foo(bool value) { return value ? "true" : "false"; }
    std::string foo(int  value) { return value ? "true" : "false"; }

    // Ensure that the class is polymorphic so we can use dynamic_cast
    virtual ~Defaults() {};
};

template <class Derived>
struct Base : Defaults {
    template <class T>
    using foo_op_b = std::integral_constant<std::string(T::*)(bool), &T::foo>;

    template <class T>
    using foo_op_i = std::integral_constant<std::string(T::*)(int), &T::foo>;

    std::string foo(bool value) {
        auto method = dispatcher_t<foo_op_b, Derived, Defaults>::value;
        auto *target = dynamic_cast<decltype(method_cls(method)) *>(this);
        return (target->*method)(value);
    }

    std::string foo(int value) {
        auto method = dispatcher_t<foo_op_i, Derived, Defaults>::value;
        auto *target = dynamic_cast<decltype(method_cls(method)) *>(this);
        return (target->*method)(value);
    }
};

struct Derived : Base<Derived> {
    std::string foo(bool value) { return value ? "TRUE" : "FALSE"; }
};

int main() {
    Derived d;
    std::cout << dynamic_cast<Base<Derived> *>(&d)->foo(true) << std::endl; // TRUE
    std::cout << dynamic_cast<Base<Derived> *>(&d)->foo(1) << std::endl;    // true
}

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

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