Перетворення звичайного класу в дивно повторюється шаблон

Деякий час тому до мене підійшли програмісти, нещодавно почали вивчати з++ і звикли до процедурного програмування, з скаргою на те, що механізм виклику віртуальних методів «гальмує». Я був дуже здивований.

Давайте згадаємо, як працюють віртуальні методи. Якщо клас містить один або більше віртуальний метод, компілятор для такого класу створює таблицю віртуальних методів, а сам клас додає віртуальний табличний курсор. Компілятор також генерує код в конструкторі класу для ініціалізації віртуального табличного покажчика. Вибір викликається віртуального методу проводиться на етапі виконання програми за допомогою вибору адреси методу створеної таблиці.

Разом маємо наступні додаткові витрати:

1) Додатковий покажчик у класі (покажчик на таблицю віртуальних методів);
2) Додатковий код в конструкторі класу (для ініціалізації віртуального табличного покажчика);
3) Додатковий код при кожному виклику віртуального методу (розіменування вказівника на таблицю віртуальних методів і пошук по таблиці потрібної адреси віртуального методу).

На щастя, компілятори зараз підтримують таку оптимізацію як девіртуалізація (devirtualization). Суть її полягає в тому, що віртуальний метод викликається безпосередньо, якщо компілятор точно знає тип викликається об'єкта і таблиця віртуальних методів при цьому не використовується. Така оптимізація з'явилася досить давно. Наприклад, для gcc — починаючи з версії 4.7, для clang'a починаючи з версії 3.8 (з'явився прапор -fstrict-vtable-pointers).

Але все ж, чи можна користуватися поліморфізмом без віртуальних функцій взагалі? Відповідь: так, можна. На допомогу приходить так званий «дивно повторюваний візерунок» (Curiously Recurring Template Pattern або CRTP).

Давайте розглянемо приклад перетворення класу з віртуальними методами в клас з шаблоном:

class IA {
public:
virtual void helloFunction() = 0;
};

class B : public IA {
public:
helloFunction(){
std::cout<< "Hello from B";
}
};

Перетворюється на:

template < typename T>
class IA {
public:
helloFunction(){
static_cast<T*>(this)->helloFunction();
}
};

class B : public IA<B> {
public:
helloFunction(){
std::cout<< "Hello from B";
}
};

Звернення:

template < typename T>
void sayHello(IA<T>* object) {
object->helloFunction();
}

Клас IA приймає шаблоном породжений клас і кастует вказівник на this до породженому класу. static_cast виробляє перевірку приведення на рівні компіляції, отже, не впливає на продуктивність. Клас B породжений від класу IA, який в свою чергу шаблонизирован класом B.

Додаткові витрати — додатковий покажчик у класі, додатковий код в конструкторі класу, додатковий код при кожному виклику віртуального методу, як у першому випадку відсутні. Якщо ваш компілятор не підтримує оптимізацію девиртуализации, то такий код буде працювати швидше і займати менше пам'яті.

Дякую за увагу.

Сподіваюся, кому-небудь замітка буде корисна.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.