novtable оптимизация


Компилятор 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
…


Вспомним, что добавляется при объявлении виртуальной функции в классе:

  1. Определение таблицы виртуальных функций. Используется один экземпляр этой таблицы для всех экземпляров класса.
  2. В члены данных класса добавляется указатель на таблицу виртуальных функций.
  3. Код по инициализации этого указателя в конструкторе класса.

Таким образом, в нашем примере будет существовать декларация двух таблиц виртуальных функций: для 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.

Выводы


  1. __declspec(novtable) — никак не влияет на объем памяти, занимаемый экземплярами класса.
  2. Уменьшение размера и некоторое ускорение работы программы обеспечивается за счет исключения определения неиспользуемых таблицы виртуальных функций, служебных данных RTTI и исключения кода инициализации указателя на таблицу виртуальных функций в конструкторах интерфейсных классов.
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 49

    +4
    Красота! Ещё чуть-чуть и они догонят Turbo Pascal 5.5, выпущенный 30 лет назад. Который просто инициализировал vtbl при создании класса один раз (независимо от схемы наследования). Потом эту «фичу» переняла Java…
      0
      Но ведь в Turbo Pascal 5.5 инициализация vptr — тоже задача конструктора; значит, конструктор предка, вызванный конструктором потомка, все равно будет ее выполнять, разве нет? Все равно получаются «напрасные» присваивания.
        +3
        Нет, не будет. Конструктор в Turbo Pascal вызывается после того, как память аллоцирована и vptr прописан. И в C++, обычно, тоже порождается не один конструктор и не один деструктор. Вот тут — на два класса три конструктора и пять (sic!) деструкторов.

        Конкретная стратегия зависит от компилятора, но из-за вот этого вот самого идиотского и никому не нужного требования — получаются ощутимые потери.

        Я на эту тему высказывался много раз: всё это нужно только и исключительно для того, чтобы избежать виртуального вызова функции из конструктора. Что является дикостью несусветной. Похоже на то, как если бы в суровый вездеход для суровых условий типа Харьковчанки поставили в угол креслице для новорожденного.
          0
          Спасибо, теперь все понятно.
            0
            Вот тут — на два класса три конструктора и пять (sic!) деструкторов.

            да, но на самом деле нет. Если убрать виртуальное наследование (которое в реальном коде практически не встречается) будет два конструктора и четыре деструктора
              0
              Удвоенное количество деструкторов всё равно остаётся. И лишние таблицы, которые прописываются — тоже.

              Главная проблема даже не в том, что всё это ресурсоёмко, а в том, кто нарушается базовый принцип: «вы не платите за то, что не заказывали».

              То, что его нарушает RTTI и исключения — всем известно. А вот то, что простые такие, бесхитростные, объекты — этом тоже страдают… про это знают немногие.
                0
                Если убрать виртуальное наследование (которое в реальном коде практически не встречается)

                Его не надо убирать, это крайне полезная штука. Виртуальное наследование позволяет реализовывать механизм интерфейсов (принципы SOLID, всё такое). Взгляните на тот же C#, Java, Delphi/Builder. В C++ же виртуальное наследование почему-то не пользуется популярностью.

                  0
                  тот же C#, Java, Delphi
                  Не имеют виртуального наследования, а имеют множественное наследование от интерфейсов.
                    0
                    Не имеют виртуального наследования, а имеют множественное наследование от интерфейсов.

                    множественное виртуальное наследование от интерфейсов

                      0
                      Нет. Интерфейсам не нужно виртуального наследования. Так как они не содержат данных.

                      То, что в C++ наследование интерфейсов приходится имитировать через виртуальное наследование — ограничение C++.
                        0
                        Нет. Интерфейсам не нужно виртуального наследования. Так как они не содержат данных.

                        Вообще-то указатель на таблицу виртуальных функций — это тоже поле данных, просто скрытое от программиста.


                        И, кстати, как же вы без виртуального наследования будете разруливать ромбовидную иерархию интерфейсов?


                        То, что в C++ наследование интерфейсов приходится имитировать через виртуальное наследование — ограничение C++.

                        Да, причём получается не очень эффективно. Сложная иерархия интерфейсов приводит к тому, что в объекте вместо одного vptr оказывается несколько, из-за чего размер объекта нехило так разрастается.


                        В языках же с нативной поддержкой интерфейсов vptr всегда один.

                          0
                          И, кстати, как же вы без виртуального наследования будете разруливать ромбовидную иерархию интерфейсов?
                          С данными проблема в том, что они должны быть согласованы (поле, записанное через левую сторону ромба, должно прочитаться через правую). Со ссылками на методы этой проблемы нет, они определяются на этапе компиляции и не меняются. Можно составить плоский список всех классов, к которым динамически кастится наш объект, и в объект положить соответствующее количество указателей на vtable, в которых сохранены указатели на методы объекта (дублирующиеся, чего нельзя было делать для виртуального наследования данных)
                            0
                            С данными проблема в том, что они должны быть согласованы (поле, записанное через левую сторону ромба, должно прочитаться через правую). Со ссылками на методы этой проблемы нет, они определяются на этапе компиляции и не меняются.

                            Если метод виртуальный, то ссылка на него определяется в процессе выполнения программы и лежит в vptr. То же самое происходит при доступе к полям при виртуальном наследовании.


                            Можно составить плоский список всех классов, к которым динамически кастится наш объект, и в объект положить соответствующее количество указателей на vtable, в которых сохранены указатели на методы объекта (дублирующиеся, чего нельзя было делать для виртуального наследования данных)

                            Можно, но тогда объект будет занимать слишком много места в памяти из-за большого количества vptr.


                            В случае .NET же этот указатель единственный, просто реализация таблицы становится гораздо более сложной:
                            https://stackoverflow.com/questions/9808982/clr-implementation-of-virtual-method-calls-to-interface-members

                              0
                              Если метод виртуальный, то ссылка на него определяется в процессе выполнения программы и лежит в vptr
                              Хм, по вашему,
                              struct A {
                                  virtual void Draw() { }
                              };
                              struct B: public A { }

                              Это и есть виртуальное наследование в C++, потому что ссылка на метод определяется в процессе выполнения программы и лежит в vptr? А это тогда как называется:
                              struct B: public virtual A


                              То же самое происходит при доступе к полям при виртуальном наследовании.

                              Не то же самое. Для данных нужно 2 уровня косвенности — считываем адрес vtable, из неё смещение поля в классе, и только потом данные. Для методов только 1 уровень.

                              Можно, но тогда объект будет занимать слишком много места в памяти из-за большого количества vptr.
                              Это всё оптимизируется, сворачивая линейные участки до 1 записи, оставляя только развилки (если B наследуется от A, достаточно подставить vptr от B, и он будет совместим по разметке с vptr A).
                                0
                                ссылка на метод определяется в процессе выполнения программы и лежит в vptr?

                                Ссылка на метод определяется в процессе компиляции, но подстановка ссылки на vptr — в процессе работы программы. Причём что при обычном наследовании, что при виртуальном.


                                Не то же самое. Для данных нужно 2 уровня косвенности — считываем адрес vtable, из неё смещение поля в классе, и только потом данные. Для методов только 1 уровень.

                                А вот и нет: https://stackoverflow.com/questions/30870096/c-virtual-inheritance-memory-layout


                                Для данных — один уровень косвенности: получение смещения дочернего объекта из vtable текущего объекта.


                                Для методов — два уровня: получение смещения дочернего объекта и поиск метода в таблице виртуальных методов дочернего объекта.

                                  0
                                  Ссылка на метод определяется в процессе компиляции, но подстановка ссылки на vptr — в процессе работы программы. Причём что при обычном наследовании, что при виртуальном.
                                  Тут вопрос в терминологии — class B: public A — это виртуальное наследование, по-вашему?

                                  Для данных — один уровень косвенности: получение смещения дочернего объекта из vtable текущего объекта.
                                  Имея ссылку на объект, для чтения поля нужно сделать 2 лишних чтения:
                                  1) vptr
                                  2) смещение поля
                                  3) чтение значения (уже не «лишнее», необходимо и без наследований).
                                  Для методов — два уровня: получение смещения дочернего объекта и поиск метода в таблице виртуальных методов дочернего объекта.
                                  Для получения смещения не нужно делать чтение из памяти — оно известно при компиляции
                                    0
                                    Тут вопрос в терминологии — class B: public A — это виртуальное наследование, по-вашему?

                                    Нет, конечно. А причём тут это?


                                    Для получения смещения не нужно делать чтение из памяти — оно известно при компиляции

                                    При виртуальном наследовании это смещение зависит от родительского объекта и хранится в vtable.

                                      0
                                      Нет, конечно. А причём тут это?
                                      При том, что

                                      И, кстати, как же вы без виртуального наследования будете разруливать ромбовидную иерархию интерфейсов?

                                      Я считаю, что в конструкции типа
                                      class A: IDrawable, ISerializable
                                      нет виртуального наследования, но возможна ромбовидная иерархия интерфейсов.

                                      При виртуальном наследовании это смещение неизвестно во время компиляции и хранится в vtable.
                                      А пример кода можете привести?
                                        0
                                        Я считаю, что в конструкции типа
                                        class A: IDrawable, ISerializable
                                        нет виртуального наследования, но возможна ромбовидная иерархия интерфейсов.

                                        Это исключительно вопрос терминологии.


                                        А пример кода можете привести?

                                        Да легко:


                                        Код
                                        #include <iostream>
                                        
                                        struct A
                                        {
                                            int f;
                                            virtual ~A() {}
                                        };
                                        
                                        struct B : public virtual A
                                        {
                                            virtual ~B() {}
                                        };
                                        
                                        struct C : public virtual A
                                        {
                                            virtual ~C() {}
                                        };
                                        
                                        struct D : public virtual B, public virtual C
                                        {
                                            virtual ~D() {}
                                        };
                                        
                                        void PrintOffset(C &c)
                                        {
                                            std::cout << "Offset = " << (char *)&c.f - (char *)&c << "\n";
                                        };
                                        
                                        int main (int argc, char **argv)
                                        {
                                            C c {};
                                            D d {};
                                        
                                            PrintOffset(c);
                                            PrintOffset(d);
                                        
                                            return 0;
                                        }

                                        Результат:


                                        Offset = 16
                                        Offset = -8
                                          0
                                          А пример кода можете привести?
                                          Да легко:
                                          Для полей данных — понятно, что так будет. Мы же вызов метода обсуждаем, обеспечивается ли он тем же самым виртуальным наследованием. Покажите пример кода, где будет работать это ваше

                                          Для методов — два уровня: получение смещения дочернего объекта и поиск метода в таблице виртуальных методов дочернего объекта.
                                            0
                                            Мы же вызов метода обсуждаем, обеспечивается ли он тем же самым виртуальным наследованием.

                                            Это зависит от реализации. Если реализация предполагает дублирование указателей в vtable родительского объекта, то будет один уровень индирекции.

                                              0
                                              Навряд ли есть реализации, не дублирующие в vtable указатели на функции родительского объекта.

                                              И причин тому несколько:
                                              1) При вызове метода появляется лишний уровень индирекции.
                                              2) Сами объекты сильно раздуваются при обычном линейном наследовании, даже не множественном и не виртуальном. Если глубина линейной иерархии — 5, потребуется 5 vptr, когда в стандартной реализации только 1.
                                              3) vtable одна на класс, и бессмысленно экономить на её длине в ущерб увеличения размера объекта или сложности кода.
                                                0
                                                2) Сами объекты сильно раздуваются при обычном линейном наследовании, даже не множественном и не виртуальном. Если глубина линейной иерархии — 5, потребуется 5 vptr, когда в стандартной реализации только 1.

                                                При линейной иерархии нет никаких проблем всё держать в одной таблице. Но при множественном наследовании каждый дочерний объект, имеющий виртуальные методы, будет иметь отдельный vptr.


                                                Решить проблему с увеличением размера объекта в этом случае можно (см. C#/Java), но ценой усложнения кода и падения производительности.


                                                Так как C++ предназначен для написания, в первую очередь, высокопроизводительного кода, активно задействуется статический полиморфизм (вместо множественного наследования интерфейсов — концепты), то множественное наследование проще объявить антипаттерном, приводящим к падению производительности, увеличению потребления памяти и усложнению логики программы.

                    0
                    Его не надо убирать, это крайне полезная штука. Виртуальное наследование позволяет реализовывать механизм интерфейсов (принципы SOLID, всё такое)

                    в с++, виртуальное наследование по сравнению с обычным наследованием от классов с виртуальными функциями позволяет одну и только одну возможность — реализовывать ромбовидное наследование. Всё. SOLID тут совсем не причем. Не путайте наследование от классов с виртуальными методами и виртуальное наследование.

                    Взгляните на тот же C#, Java

                    в c#/java нет множественного наследования как такового, а значит, там нет и ромбовидного наследования. В любом случае, множественное наследование считается антипаттерном, отчего и ромбовидное встречается (ну, по крайней мере должно встречаться) крайне редко.
                      0
                      SOLID тут совсем не причем

                      Прнципы SOLID призывают вместо одного большого интерфейса создавать множество мелких. Виртуальное наследование позволяет не заботиться о том, что один и тот же интерфейс может встретиться несколько раз.


                      в c#/java нет множественного наследования как такового, а значит, там нет и ромбовидного наследования

                      Неверно. Если оперерировать терминами C++, то в C#/Java интерфейсы наследуются множественно и всегда виртуально.


                      В любом случае, множественное наследование считается антипаттерном, отчего и ромбовидное встречается (ну, по крайней мере должно встречаться) крайне редко.

                      Не согласен. Вот пример, где без ромбовидного наследования ну вообще никуда:


                      interface IEnumerable<T> {}
                      interface IReadOnlyCollection<T> : IEnumerable<T> {}
                      interface ICollection<T> : IReadOnlyCollection<T> {}
                      interface IReadOnlyList<T> : IReadOnlyCollection<T> {}
                      interface IList<T> : IReadOnlyList<T>, ICollection<T> {}
                        0
                        Прнципы SOLID призывают вместо одного большого интерфейса создавать множество мелких

                        если возводить принцип interface segregation в абсолют, создавая по одному интерфейсу на каждый публичный метод класса, а потом комбинируя их всеми возможными способами через наследование, то да, придется иметь дело и с ромбовидным. Посмею предположить, что этот принцип неудачно сформулирован.

                        Не согласен. Вот пример, где без ромбовидного наследования ну вообще никуда:

                        в с++ (о котором идет речь) конкретно для этого набора (реализации списка и других контейнеров) можно обойтись вообще без наследования. Касательно виртуального наследования — лично мне за всю карьеру оно понадобилось дважды, и оба использования я считаю скорее вынужденными хаками, нежели элегантной архитектурой. Более того, даже сам термин «виртуальное наследование» существует только применительно к с++.
                  0
                  И это является хорошим примером, показывающим, почему множественное наследование классов (ещё и виртуальное) — зло.

                0
                Паскаль не догонишь, там есть checked arithmetics и helper классы.
                +4
                В C++ не хватает такой сущности как interface, — структурой в которой такая оптимизация была бы включена по умолчанию и компилятор, который выдавал ошибку если в интерфейс добавить что либо кроме виртуальных функций.
                  0
                  А кто мешает компилятору понять, что у класса нет реализаций вирт. ф-ций (т.е., это абстрактный класс) и не инициализировать vtable и заодно конструктор не создавать, чтобы наследники этот конструктор не вызывали.
                    0
                    Наверное то, что если вы захотите привести указатель на интерфейс к его реализации (dynamic_cast'ом), то программа проведёт себя очень странно.
                      0
                      И почему же? Корректный указатель на интерфейс всегда фактически указывает на наследника (реализацию), который имеет корректный vtable, инициализированный в конструкторе наследника.
                        0
                        Да, соглашусь, поторопился.
                        vtable один на объект (для каждого интерфейса), корректно инициализированный наследниками, и множественное/виртуальное наследование тут ничего не сломает при хождении по иерархии.
                          0
                          Не всегда. Во время работы конструктора и деструктора абстрактного класса (а они абстрактными быть не могут) — vtable должна указывать на сам этот класс.

                            0
                            Во время работы конструктора и деструктора абстрактного класса (а они абстрактными быть не могут)
                            Почему вы считаете, что у абстрактного класса обязаны быть конструктор и деструктор?
                              +1
                              Потому что так устроен C++, однако. Теоретически да, можно сделать невиртуальный деструктор и гарантировать как-нибудь, что через ваш интерфейс объект никогда-никогда не удалят… Практически — это слишком опасно.
                                0
                                Практически — это слишком опасно
                                Единственный проблемный случай — это инстанциирование абстрактного класса в куче (а зачем это нужно?) и затем его удаление в контексте, когда компилятор не знает тип объекта (чтобы не применил virtual call elimination). В этом случае у класса не заполнен vtable, и вызов вирт. метода упадёт. Например,
                                Скрытый текст
                                struct IDrawable
                                {
                                        virtual void draw() = 0;
                                        virtual ~IDrawable() { }
                                };
                                
                                __declspec(noinline) void del(IDrawable* e) { delete e; }
                                
                                int main()
                                {
                                        IDrawable* abstract = new IDrawable();
                                        del(abstract);
                                }
                                А, нет. Такое не компилируется — error C2259: 'IDrawable': cannot instantiate abstract class

                                В остальных случаях, когда в абстрактном классе есть вирт. деструктор, а по интерфейсной ссылке передан объект-наследник (у которого vtable уже содержит адрес своего деструктора), никаких проблем с отсутствием конструктора и vtable у класса-интерфейса.
                                  0
                                  В остальных случаях, когда в абстрактном классе есть вирт. деструктор
                                  То у нас в абстрактном классе появится неабстрактный метод. Об чём и речь.

                                  никаких проблем с отсутствием конструктора и vtable у класса-интерфейса.
                                  А что будет в vtable во время работы вышеупомянутого деструктора, извините?
                                    0
                                    А что будет в vtable во время работы вышеупомянутого деструктора, извините?
                                    Вы очень хорошо умеете находить крайние тестовые случаи )))

                                    Но всё равно, оптимизация, которая убирает необходимость в концепциях interface и __desclspec(novtable), возможна, и её можно сформулировать следующим образом:

                                    — если класс содержит только абстрактные виртуальные методы (кроме, возможно, деструктора)
                                    — и деструктор (возможно, виртуальный) либо отсутствует, либо пустой (либо = default, что характерно для интерфейсов)
                                    — то можно в конструкторе такого класса не инициализировать vtable. Соответственно, такой конструктор, который раньше состоял только из инициализации vtable, может вообще исчезнуть, и в конструкторах классов-наследников его вызов тоже можно удалить.

                                    Вроде, всё предусмотрел?
                                      0
                                      Вы очень хорошо умеете находить крайние тестовые случаи )))
                                      Я просто несколько лет назад «смотрел на молоток со стороны гвоздя» — в смысле работал с компилятором и чинил там разные странности.

                                      А так — да, в некоторых случаях действительно можно избавиться от конструктора/деструктора и vtable. Примерно в описанных вами случаях — посмотрите на пример, который мы уже обсуждали.

                                      В функции bar никакого двойного переписывания vptr нету. А почему тогда сгенерированы все эти бесконечные конструкторы и деструкторы? А потому что ABI так говорит: вдруг другой компилятор решит их вызвать?

                                      Так что описанное в статье — это, грубо говоря, обещание компилятору: «друг, верь, я не идиот, вот эту вот всю муть, которая никому не нужна — можешь выкидывать смело».

                                      В Linux принят другой подход: если сделать вот так — то все эти бесконечные таблицы по-прежнему будут сгенерированы, но линкер сможет от них избавится…
                                        +1
                                        А почему тогда сгенерированы все эти бесконечные конструкторы и деструкторы? А потому что ABI так говорит: вдруг другой компилятор решит их вызвать?
                                        эти методы может выкинуть линкер при сборке exe. Для dll останутся, но это какая-то ерундовая экономия, стоит ли она поддержки в компиляторе.
                                          +1
                                          Ну это у разработчиков Visual Studio нужно спросить. Это ж новейшая технология нужна! В Turbo Pascal она появилась в версии 4.0, 1987й год (и была включена по умолчанию!), в gcc — тоже в прошлом веке (но до сих пор не включается автоматом, ручками нужно заказывать), а MSVC, похоже, ниасилил…
                                        0
                                        Сейчас, вроде, все.
                                        Только этот случай покрывается уже существующими оптимизациями (инлайнинг конструктора базового класса + удаление лишнего присвоения + удаление «мертвого» кода конструктора базового класса).
                                        godbolt.org/z/HxCGjt
                          +1
                          Дык конструктор-то быть как раз обязан… И виртуальная функция, как минимум одна, таки есть: деструктор…
                            0
                            Дык конструктор-то быть как раз обязан
                            Для чего классу-интерфейсу конструктор?
                            И виртуальная функция, как минимум одна, таки есть: деструктор
                            Не нужна. Вот пример:
                            Скрытый текст
                            struct IDrawable
                            {
                                    virtual void draw() = 0;
                            };
                            
                            class Line: public IDrawable
                            {
                                    void draw() override { };
                            };
                            
                            class Rect: public IDrawable
                            {
                                    void draw() override { };
                            };
                            
                            int main()
                            {
                                    Line l;
                                    Rect r;
                                    IDrawable* objs[2] = { &l, &r };
                                    for (auto& e : objs) e->draw();
                                    return 0;
                            }
                              0
                              Ну если вам так хочется выстрелить себе в ногу, то в C++ есть много других способов. Заметьте, что вы теперь не можете передать куда-то IDrawable и потом от этого объекта избавиться: удалять его должен тот же, кто его создал. В большинстве случаев так использовать интерфейсы черезвычайно неудобно. А если вы создадите виртуальный деструктор (чтобы ваши Line и Rect освобождались как Line и Rect при доступе через IDrawable) — то получите весь букет обсуждающихся проблем… даже если этот виртуальный деструктор будет пустой…
                                0
                                Заметьте, что вы теперь не можете передать куда-то IDrawable и потом от этого объекта избавиться: удалять его должен тот же, кто его создал
                                Можно и через интерфейс удалять, без вирт. деструктора — IUnknown::Release ))

                                В остальном вы не правы. Детально ответил выше.
                                  0
                                  Можно и через интерфейс удалять, без вирт. деструктора — IUnknown::Release ))
                                  Который вам придётся отдельно реализовывать в каждом потомке. Да, так тоже можно. Но C++ достаточно сложен и без того, чтобы самому себе создавать подобные проблемы.
                          0
                          Есть такая штука — C++ Builder, которая умеет делать подобное. Отдельного ключевого слова для интерфейсов в нём нет, но если класс удовлетворяет свойствам интерфейса, то он реализуется особым образом.

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое