Некоторое время назад ко мне обратились программисты, недавно начавшие изучать C++ и привыкшие к процедурному программированию, с жалобой на то, что механизм вызова виртуальных методов «медленный».
Я был очень удивлен.
Давайте вспомним, как работают виртуальные методы.
Если класс содержит один или несколько виртуальных методов, компилятор создает таблицу виртуальных методов для этого класса и добавляет указатель виртуальной таблицы на сам класс.
Компилятор также генерирует код в конструкторе класса для инициализации указателя виртуальной таблицы.
Выбор вызываемого виртуального метода осуществляется на этапе выполнения программы путем выбора адреса метода из созданной таблицы.
Итого имеем следующие дополнительные расходы: 1) Дополнительный указатель в классе (указатель на таблицу виртуальных методов); 2) Дополнительный код в конструкторе класса (для инициализации указателя виртуальной таблицы); 3) Дополнительный код для каждого вызова виртуального метода (разыменование указателя на таблицу виртуальных методов и поиск в таблице нужного адреса виртуального метода).
К счастью, компиляторы теперь поддерживают такие оптимизации, как девиртуализация.
Суть его в том, что виртуальный метод вызывается напрямую, если компилятор точно знает тип вызываемого объекта и не используется таблица виртуальных методов.
Эта оптимизация существует уже довольно давно.
Например, для gcc — начиная с версии 4.7, для clang начиная с версии 3.8 (появился флаг -fstrict-vtable-pointers).
Но все же, можно ли вообще использовать полиморфизм без виртуальных функций? Ответ: да, можете.
На помощь приходит так называемый Curiously Recurring Template Pattern (CRTP).
Правда, это уже будет статический полиморфизм.
Он отличается от обычного динамического.
Давайте рассмотрим пример преобразования класса с виртуальными методами в класс с шаблоном:
Превращается в:class IA { public: virtual void helloFunction() = 0; }; class B : public IA { public: void helloFunction(){ std::cout<< "Hello from B"; } };
template <typename T>
class IA {
public:
void helloFunction(){
static_cast<T*>(this)->helloFunction();
}
};
class B : public IA<B> {
public:
void helloFunction(){
std::cout<< "Hello from B";
}
};
Обращаться:
template <typename T>
void sayHello(IA<T>* object) {
object->helloFunction();
}
Класс IA принимает сгенерированный класс в качестве шаблона и передает указатель на него в сгенерированный класс.
static_cast выполняет проверку приведения на уровне компиляции и поэтому не влияет на производительность.
Класс B является производным от класса IA, который, в свою очередь, создан по шаблону класса B. Дополнительные затраты — дополнительный указатель в классе, дополнительный код в конструкторе класса, дополнительный код для каждого вызова виртуального метода, как и в первом случае, отсутствуют. Если ваш компилятор не поддерживает оптимизацию девиртуализации, то такой код будет работать быстрее и занимать меньше памяти.
Спасибо за внимание.
Надеюсь, эта заметка будет кому-то полезна.
Теги: #c plus plus #программирование #шаблоны C++ #виртуальные функции #C++
-
Alphago Выиграла Вторую Игру У Ли Седоля
19 Oct, 24 -
Mfcast #21: Специально Для Ос Google Android
19 Oct, 24