Допустим, у вас есть класс, который имеет несколько вариантов реализации, и по какому-то условию инстанцироваться должна только одна из реализаций. Например, как в шаблоне проектирования "Стратегия".
Разумеется, стандартное ООП-решение
float s;
enum Types { Type1, Type2 };
class MyClass
{
class Implementation
{
public:
virtual void f() = 0;
virtual ~Implementation() {}
};
class Implementation1 : public Implementation
{
public:
virtual void f() { s += 1.0f; }
};
class Implementation2 : public Implementation
{
public:
virtual void f() { s -= 1.0f; }
};
Implementation *implementation;
public:
MyClass(const Types type) : implementation(NULL)
{
switch(type)
{
case Type1: implementation = new Implementation1(); break;
case Type2: implementation = new Implementation2(); break;
default: throw exception();
}
}
void f()
{
implementation->f();
}
~MyClass()
{
delete implementaiton;
}
};
с реализацией интерфейса приводит к тому, что на каждый вызов MyClass::f приходится разыменовывать два указателя: сам объект implementation, и потом в нём виртуальную функцию. В результате, например, миллиард вызовов работает примерно 23-24 секунды вместо 4 секунд обычной функции:
void f()
{
s += 1.0f;
}
Что же с этим делать?
Желание хотя бы заранее разыменовать указатель на implementation сопряжено с трудностями. Объект типа IImpl существовать не может - из-за абстрактного класса. Может существовать только ссылка, но её обязательно нужно инициализировать в конструкторе, да ещё и в списке инициализации. Поэтому можно соорудить конструктор с созданием объекта реализации при помощи вспомогательной статической функции-фабрики, а ссылку на этот объект инициализировать сразу после его создания:
class MyClass
{
/* ... */
Implementation *implementation;
Implementation &reference;
static Implementation *createImplementation(const Types type)
{
switch(type)
{
case Type1: return new Implementation1();
case Type2: return new Implementation2();
default: throw exception();
}
}
public:
MyClass(const Types type) : implementation(createImplementation(type)), reference(*implementation)
{}
void f()
{
reference.f();
}
~MyClass()
{
delete implementaiton;
}
};
И надо заметить, это нам почти никак не помогло - основной вклад в падение производительности привносит именно вызов виртуальной функции.
Разные реализации находятся в разных классах. Свести типы этих классов к единому, чтобы назначить этот тип полю класса, можно только наследованием. Появившийся в C++11 вариант с auto применим лишь к локальным переменным внутри функций, но не к членам класса и не к возвращаемым значениям функций. Отказаться от виртуальных функций тоже нельзя - не будут вызываться методы реализаций. Вовсе отказаться от типизации сложно: во-первых, нетипизированным бывает лишь указатель, но не ссылка, во-вторых, для вызова метода всё равно нужно знать тип класса. Но по крайней мере такой вариант можно попробовать: оставить лишь указатель на реализацию, а выбор виртуальной функции заменить на жёсткое условие.
class MyClass
{
class Implementation1
{
public:
void f() { s += 1.0f; }
};
class Implementation2
{
public:
void f() { s -= 1.0f; }
};
void *implementation;
public:
MyClass(const Types type) : implementation(NULL)
{
switch(type)
{
case Type1: implementation = new Implementation1(); break;
case Type2: implementation = new Implementation2(); break;
default: throw exception();
}
}
void f()
{
switch(type)
{
case Type1: static_cast<Implementation1 *>(implementation)->f(); break;
case Type2: static_cast<Implementation2 *>(implementation)->f(); break;
default: throw exception();
}
}
~MyClass()
{
delete implementaiton;
}
};
И этот вариант работает лишь чуть медленнее простой функции f: чуть больше 4 секунд. Обработка жёсткого условия происходит гораздо быстрее выборки из таблицы виртуальных функций классов.
Последний вариант: использовать идею шаблона проектирования "Lightweight" для случая инициализации. Инстанцировать все варианты реализации сразу, но сделать их лёгкими объектами, отложив инициализацию до момента выбора рабочего варианта. Затем, как и в предыдущем случае, использовать жёсткое условие:
class MyClass
{
class Implementation1
{
public:
Implementation1() { /* Тут пусто */ }
void init() { /* Тут основная инициализация */ }
void f() { s += 1.0f; }
};
class Implementation2
{
public:
Implementation2() { /* Тут пусто */ }
void init() { /* Тут основная инициализация */ }
void f() { s -= 1.0f; }
};
Implementation1 implementation;
Implementation2 implementation;
public:
MyClass(const Types type)
{
/* Выбираем, какой объект будет рабочим, остальные остаются неинициализированными и "лёгкими". */
switch(type)
{
case Type1: implementation1.init(); break;
case Type2: implementation2.init(); break;
default: throw exception();
}
}
void f()
{
switch(type)
{
case Type1: implementation1.f(); break;
case Type2: implementation2.f(); break;
default: throw exception();
}
}
};
И этот вариант работает так же быстро, как простая функция - около 4 секунд.