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

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


  1. khim
    02.03.2019 14:46
    -1

    Красота! Ещё чуть-чуть и они догонят Turbo Pascal 5.5, выпущенный 30 лет назад. Который просто инициализировал vtbl при создании класса один раз (независимо от схемы наследования). Потом эту «фичу» переняла Java…


    1. iliazeus
      02.03.2019 18:17

      Но ведь в Turbo Pascal 5.5 инициализация vptr — тоже задача конструктора; значит, конструктор предка, вызванный конструктором потомка, все равно будет ее выполнять, разве нет? Все равно получаются «напрасные» присваивания.


      1. khim
        02.03.2019 19:10
        +1

        Нет, не будет. Конструктор в Turbo Pascal вызывается после того, как память аллоцирована и vptr прописан. И в C++, обычно, тоже порождается не один конструктор и не один деструктор. Вот тут — на два класса три конструктора и пять (sic!) деструкторов.

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

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


        1. iliazeus
          02.03.2019 19:21

          Спасибо, теперь все понятно.


        1. Antervis
          02.03.2019 19:33

          Вот тут — на два класса три конструктора и пять (sic!) деструкторов.

          да, но на самом деле нет. Если убрать виртуальное наследование (которое в реальном коде практически не встречается) будет два конструктора и четыре деструктора


          1. khim
            02.03.2019 19:46

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

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

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


          1. DistortNeo
            03.03.2019 13:37

            Если убрать виртуальное наследование (которое в реальном коде практически не встречается)

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


            1. qw1
              03.03.2019 14:23

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


              1. DistortNeo
                03.03.2019 14:28

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

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


                1. khim
                  03.03.2019 14:39

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

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


                  1. DistortNeo
                    03.03.2019 15:04

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

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


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


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

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


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


                    1. qw1
                      03.03.2019 16:46

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


                      1. DistortNeo
                        03.03.2019 17:32

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

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


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

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


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


                        1. qw1
                          03.03.2019 19:05

                          Если метод виртуальный, то ссылка на него определяется в процессе выполнения программы и лежит в 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).


                          1. DistortNeo
                            03.03.2019 19:54

                            ссылка на метод определяется в процессе выполнения программы и лежит в vptr?

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


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

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


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


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


                            1. qw1
                              03.03.2019 22:44

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

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


                              1. DistortNeo
                                03.03.2019 23:29

                                Тут вопрос в терминологии — class B: public A — это виртуальное наследование, по-вашему?

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


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

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


                                1. qw1
                                  03.03.2019 23:34

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

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

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

                                  При виртуальном наследовании это смещение неизвестно во время компиляции и хранится в vtable.
                                  А пример кода можете привести?


                                  1. DistortNeo
                                    03.03.2019 23:42

                                    Я считаю, что в конструкции типа
                                    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;
                                    }


                                    1. qw1
                                      04.03.2019 07:53

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

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


                                      1. DistortNeo
                                        04.03.2019 09:04

                                        Мы же вызов метода обсуждаем, обеспечивается ли он тем же самым виртуальным наследованием.

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


                                        1. qw1
                                          04.03.2019 10:13

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

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


                                          1. DistortNeo
                                            04.03.2019 11:39

                                            2) Сами объекты сильно раздуваются при обычном линейном наследовании, даже не множественном и не виртуальном. Если глубина линейной иерархии — 5, потребуется 5 vptr, когда в стандартной реализации только 1.

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


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


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


            1. Antervis
              03.03.2019 16:08

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

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

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

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


              1. DistortNeo
                03.03.2019 16:29

                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> {}


                1. Antervis
                  04.03.2019 02:30

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

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

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

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


        1. DistortNeo
          03.03.2019 13:12

          И это является хорошим примером, показывающим, почему множественное наследование классов (ещё и виртуальное) — зло.


    1. dim2r
      03.03.2019 12:39

      Паскаль не догонишь, там есть checked arithmetics и helper классы.


  1. Ivaneo
    02.03.2019 15:45

    В C++ не хватает такой сущности как interface, — структурой в которой такая оптимизация была бы включена по умолчанию и компилятор, который выдавал ошибку если в интерфейс добавить что либо кроме виртуальных функций.


    1. qw1
      02.03.2019 21:02

      А кто мешает компилятору понять, что у класса нет реализаций вирт. ф-ций (т.е., это абстрактный класс) и не инициализировать vtable и заодно конструктор не создавать, чтобы наследники этот конструктор не вызывали.


      1. to_climb
        02.03.2019 21:31

        Наверное то, что если вы захотите привести указатель на интерфейс к его реализации (dynamic_cast'ом), то программа проведёт себя очень странно.


        1. qw1
          02.03.2019 21:47

          И почему же? Корректный указатель на интерфейс всегда фактически указывает на наследника (реализацию), который имеет корректный vtable, инициализированный в конструкторе наследника.


          1. to_climb
            02.03.2019 22:17

            Да, соглашусь, поторопился.
            vtable один на объект (для каждого интерфейса), корректно инициализированный наследниками, и множественное/виртуальное наследование тут ничего не сломает при хождении по иерархии.


          1. khim
            03.03.2019 00:02

            Не всегда. Во время работы конструктора и деструктора абстрактного класса (а они абстрактными быть не могут) — vtable должна указывать на сам этот класс.


            1. qw1
              03.03.2019 00:12

              Во время работы конструктора и деструктора абстрактного класса (а они абстрактными быть не могут)
              Почему вы считаете, что у абстрактного класса обязаны быть конструктор и деструктор?


              1. khim
                03.03.2019 01:05

                Потому что так устроен C++, однако. Теоретически да, можно сделать невиртуальный деструктор и гарантировать как-нибудь, что через ваш интерфейс объект никогда-никогда не удалят… Практически — это слишком опасно.


                1. qw1
                  03.03.2019 08:23

                  Практически — это слишком опасно
                  Единственный проблемный случай — это инстанциирование абстрактного класса в куче (а зачем это нужно?) и затем его удаление в контексте, когда компилятор не знает тип объекта (чтобы не применил 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);
                  }


                  1. khim
                    03.03.2019 14:41

                    В остальных случаях, когда в абстрактном классе есть вирт. деструктор
                    То у нас в абстрактном классе появится неабстрактный метод. Об чём и речь.

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


                    1. qw1
                      03.03.2019 16:38

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

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

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

                      Вроде, всё предусмотрел?


                      1. khim
                        03.03.2019 17:07

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

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

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

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

                        В Linux принят другой подход: если сделать вот так — то все эти бесконечные таблицы по-прежнему будут сгенерированы, но линкер сможет от них избавится…


                        1. qw1
                          03.03.2019 19:10
                          +1

                          А почему тогда сгенерированы все эти бесконечные конструкторы и деструкторы? А потому что ABI так говорит: вдруг другой компилятор решит их вызвать?
                          эти методы может выкинуть линкер при сборке exe. Для dll останутся, но это какая-то ерундовая экономия, стоит ли она поддержки в компиляторе.


                          1. khim
                            03.03.2019 20:58
                            +1

                            Ну это у разработчиков Visual Studio нужно спросить. Это ж новейшая технология нужна! В Turbo Pascal она появилась в версии 4.0, 1987й год (и была включена по умолчанию!), в gcc — тоже в прошлом веке (но до сих пор не включается автоматом, ручками нужно заказывать), а MSVC, похоже, ниасилил…


                      1. iliazeus
                        03.03.2019 17:15

                        Сейчас, вроде, все.
                        Только этот случай покрывается уже существующими оптимизациями (инлайнинг конструктора базового класса + удаление лишнего присвоения + удаление «мертвого» кода конструктора базового класса).
                        godbolt.org/z/HxCGjt


      1. khim
        03.03.2019 00:00

        Дык конструктор-то быть как раз обязан… И виртуальная функция, как минимум одна, таки есть: деструктор…


        1. qw1
          03.03.2019 00:10

          Дык конструктор-то быть как раз обязан
          Для чего классу-интерфейсу конструктор?
          И виртуальная функция, как минимум одна, таки есть: деструктор
          Не нужна. Вот пример:
          Скрытый текст
          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;
          }


          1. khim
            03.03.2019 01:15

            Ну если вам так хочется выстрелить себе в ногу, то в C++ есть много других способов. Заметьте, что вы теперь не можете передать куда-то IDrawable и потом от этого объекта избавиться: удалять его должен тот же, кто его создал. В большинстве случаев так использовать интерфейсы черезвычайно неудобно. А если вы создадите виртуальный деструктор (чтобы ваши Line и Rect освобождались как Line и Rect при доступе через IDrawable) — то получите весь букет обсуждающихся проблем… даже если этот виртуальный деструктор будет пустой…


            1. qw1
              03.03.2019 08:24

              Заметьте, что вы теперь не можете передать куда-то IDrawable и потом от этого объекта избавиться: удалять его должен тот же, кто его создал
              Можно и через интерфейс удалять, без вирт. деструктора — IUnknown::Release ))

              В остальном вы не правы. Детально ответил выше.


              1. khim
                03.03.2019 14:44

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


    1. DistortNeo
      03.03.2019 13:02

      Есть такая штука — C++ Builder, которая умеет делать подобное. Отдельного ключевого слова для интерфейсов в нём нет, но если класс удовлетворяет свойствам интерфейса, то он реализуется особым образом.