Компилятор Microsoft позволяет добавить расширение «novtable» для атрибута «__declspec» при объявлении класса.
Заявленная цель — значительно уменьшить размер генерируемого кода. На экспериментах с нашими компонентами уменьшение составило от 0,6 до 1,2 процента от размера DLL.
Применимость: классы, не предназначенные для создания экземпляров напрямую из них.
Например: чисто интерфейсные классы.
В коде это выглядит так:
struct __declspec(novtable) IDrawable
{
virtual void Draw() const = 0;
};
Примечание: ключевое слово struct использовалось для декларации интерфейсного класса, чтобы избавить пример от не относящихся к теме статьи деталей; тогда как в случае использования class пришлось бы использовать public для указания «публичности» методов. По той же причине я не буду в этой статье добавлять виртуальный деструктор в интерфейсный класс.
Название «novtable» обещает, что виртуальной таблицы не будет… Но как же работает механизм вызова виртуальных функций в следующем коде:
// Добавим декларацию прямоугольника, реализующего интерфейс IDrawable:
class Rectangle : public IDrawable
{
virtual void Draw() const override
{
}
int width;
int height;
};
…
IDrawable* drawable = new Rectangle;
drawable->Draw(); // происходит вызов Rectangle::Draw
…
Вспомним, что добавляется при объявлении виртуальной функции в классе:
- Определение таблицы виртуальных функций. Используется один экземпляр этой таблицы для всех экземпляров класса.
- В члены данных класса добавляется указатель на таблицу виртуальных функций.
- Код по инициализации этого указателя в конструкторе класса.
Таким образом, в нашем примере будет существовать декларация двух таблиц виртуальных функций: для IDrawable и для Rectangle. При создании объекта Rectangle первым выполняется конструктор IDrawable, который инициализирует указатель на свою таблицу виртуальных функций. Схематично это выглядит так:
Так как функция draw в IDrawable объявлена чисто-виртуальной (указано "=0" вместо тела функции), то в таблице виртуальных функций записан адрес генерируемой компилятором функции purecall.
Затем выполняется конструктор Rectangle, который инициализирует тот же указатель, но на свою таблицу виртуальных функций:
Что же делает «novtable», и почему Microsoft обещает уменьшение размера кода?
Именно ненужное определение таблицы виртуальных функций IDrawable и инициализация указателя на нее в конструкторе IDrawable исключаются из результирующего кода при добавлении «novtable».
В этом случае при конструировании IDrawable указатель на таблицу виртуальных функций будет содержать непредсказуемое значение. Но это не должно нас беспокоить, так как создание реализации с обращением к виртуальным функциям до полного конструирования объекта, как правило, является ошибкой. Если, например, в конструкторе базового класса вызывать невиртуальную функцию этого класса, которая в свою очередь вызывает виртуальную функцию, то без novtable будет вызвана функция purecall, а с novtable — будет непредсказуемое поведение; ни один из вариантов не может быть приемлемым.
Заметим, что происходит не только уменьшение размера, но и некоторое ускорение работы программы.
RTTI
Как известно, std::dynamic_cast позволяет приводить указатели и ссылки одного экземпляра класса к указателю и ссылке на другой, если эти классы связаны иерархией и являются полиморфными (содержат таблицу виртуальных функций). В свою очередь оператор typeid позволяет получать в runtime информацию об объекте по переданному ему указателю (ссылке) на этот объект. Эти возможности обеспечиваются механизмом RTTI, который использует информацию о типах, расположенную с привязкой к vtable класса. Детали структуры и расположения зависят от компилятора. В случае компилятора Microsoft схематично это выглядит так:
Поэтому если при сборке компилятору приказано включить RTTI, то novtable исключает еще и создание определения type_info для IDrawable и требуемых для нее служебных данных.
Заметим, что если у вас каким-то образом обеспечивается знание, что приводимый указатель (ссылка) на базовый класс указывает на реализацию производного, то std::static_cast эффективнее и не требует RTTI.
Microsoft specific
Помимо MSVC, данная возможность с тем же самым синтаксисом присутствует в Clang при компиляции под Windows.
Выводы
- __declspec(novtable) — никак не влияет на объем памяти, занимаемый экземплярами класса.
- Уменьшение размера и некоторое ускорение работы программы обеспечивается за счет исключения определения неиспользуемых таблицы виртуальных функций, служебных данных RTTI и исключения кода инициализации указателя на таблицу виртуальных функций в конструкторах интерфейсных классов.