CRTP, щоб уникнути динамічного поліморфізму


Відповіді:


139

Є два шляхи.

Перший - це статичне вказівку інтерфейсу для структури типів:

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

Отже, поєднання визначення структури / інтерфейсу та вирахування типу компіляції у ваших функціях дозволяє виконувати статичне відправлення замість динамічного відправлення. У цьому суть статичного поліморфізму.


15
Відмінна відповідь
Елі Бендерський

5
Я хотів би наголосити, що not_derived_from_baseце не походить від baseі не походить від base...
leftarounda približno

3
Насправді декларація foo () всередині my_type / your_type не потрібна. codepad.org/ylpEm1up (Викликає переповнення стека) - Чи є спосіб застосувати визначення foo під час компіляції? - Гаразд, знайшов рішення: ideone.com/C6Oz9 - Можливо, ви хочете це виправити у своїй відповіді.
cooky451 03.03.12

3
Не могли б ви пояснити мені, яка мотивація використовувати CRTP у цьому прикладі? Якщо бар буде визначений як шаблон <клас T> void bar (T & obj) {obj.foo (); }, тоді будь-який клас, який надає foo, буде чудовим. Отже, на основі вашого прикладу виглядає так, що єдиним використанням CRTP є визначення інтерфейсу під час компіляції. Це для чого це?
Антон Данейко

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

18

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

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


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

@AntonDaneyko При виклику базового екземпляра викликається сума базового класу, наприклад, "область фігури" із реалізацією за замовчуванням, як ніби це квадрат. Ціль CRTP у цьому випадку полягає у вирішенні найбільш похідної реалізації, "області трапеції" тощо, при цьому, маючи можливість посилатися на трапецію як фігуру, поки не буде потрібно похідна поведінка. В основному, щоразу, коли вам зазвичай потрібні dynamic_castвіртуальні методи.
Джон П


-5

Ця відповідь у Вікіпедії містить усе необхідне. А саме:

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

4
Насправді дублювання бази на екземпляр шаблону є ілюзією, оскільки (якщо у вас досі немає vtable), компілятор об'єднає сховище бази та похідного в єдину структуру для вас. Виклик покажчика функції також оптимізується компілятором (частина static_cast).
Дін Майкл

19
До речі, ваш аналіз CRTP є неправильним. Це повинно бути: Пам'ять: Нічого, як сказав Дін Майкл. Час виконання: Один (швидший) виклик статичної функції, а не віртуальний, що є суттю суті вправи. static_cast нічого не робить, він просто дозволяє компілювати код.
Фредерік Слайкерман,

2
Моя думка полягає в тому, що базовий код буде продубльований у всіх екземплярах шаблону (саме те об’єднання, про яке ви говорите). Подібно до того, що шаблон має лише один метод, який покладається на параметр шаблону; все інше краще в базовому класі, інакше воно втягується (зливається) кілька разів.
user23167

1
Кожен метод у базі буде скомпільований знову для кожного похідного. У (очікуваному) випадку, коли кожен екземплярний метод відрізняється (оскільки властивості похідного відрізняються), це не обов'язково зараховується як накладні витрати. Але це може призвести до збільшення загального розміру коду проти ситуації, коли складний метод у (звичайному) базовому класі викликає віртуальні методи підкласів. Крім того, якщо ви помістите в Base <Derived> утилітні методи, які насправді взагалі не залежать від <Derived>, вони все одно отримають екземпляр. Можливо, глобальна оптимізація це дещо виправить.
greggo

Виклик, який проходить через кілька рівнів CRTP, розширюватиметься в пам'яті під час компіляції, але може легко скорочуватися через TCO та вбудовування. Тоді саме CRTP насправді не винуватець, так?
John P
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.