Таблица виртуальных методов и техника безопасности

    Техника безопасностиВ качестве небольшой разминки перед статьёй хотелось бы, чтобы читатель задал себе следующий вопрос: нужно ли фотографу для получения качественных снимков знать, как работает фотоаппарат? Ну, по крайней мере, должен ли он знать понятие «диафрагма»? «Отношение сигнал-шум»? «Глубина резкости»? Практика подсказывает, что даже со знанием таких сложных слов снимки могут получиться у наиболее «рукастых» не особо лучше снятых на мобильник через 0.3-МПикс-дупло. И наоборот, по-настоящему хорошие снимки могут получаться благодаря исключительно опыту и наитию при полном незнании матчасти (хотя это, скорее, исключения из правил, но всё же). Однако вряд ли со мной кто-то будет спорить, что профессионалам, которые хотят выжать из своей техники всё (а не только количество мегапикселей на квадратный миллиметр матрицы), эти знания нужны в обязательном порядке, поскольку в противном случае ему и называться профессионалом-то нельзя. И верно это не только для отрасли цифровой фотографии, но и для практически любой другой.

    Верно это и для программирования, а для программирования на языке С++ – вдвойне. В этой статье будет описано важное понятие языка, известное как «Виртуальный табличный указатель», что присутствует почти во всех сложных классах, и то, каким образом его можно случайно повредить. Это может, в свою очередь, вести к едва поддающимся отладке ошибкам. Сначала напомню, что это вообще такое, а затем и поделюсь своими соображениями по поводу того, как и что может там сломаться.

    К нашему огромному сожалению, в этой статье будет много рассуждений, связанных с низким уровнем. Но больше никак проблему, увы, не проиллюстрировать. Заодно оговорюсь, что статья написана по большей части для компилятора Visual C++ Compiler в режиме сборки 64-битной программы – результаты работы программы в других компиляторах и под другую архитектуру могут отличаться.

    Виртуальный табличный указатель


    В теории сказано, что указатель vptr – указатель на таблицу виртуальных методов, или виртуальный табличный указатель – присутствует в каждом классе, в котором есть хотя бы один виртуальный метод. Разберёмся поподробнее, что же это за зверь-то такой. Для этого напишем простенькую демонстрационную программу на языке С++.
    #include <iostream>
    #include <iomanip>
    using namespace std;
    int nop() {
      static int nop_x; return ++nop_x; // Не удаляй меня, компилятор!
    };
    
    class A
    {
    public:
      unsigned long long content_A;
      A(void) : content_A(0xAAAAAAAAAAAAAAAAull)
          { cout << "++ A has been constructed" << endl;};
      ~A(void) 
          { cout << "-- A has been destructed" << endl;};
    
      void function(void) { nop(); };
    };
    
    void PrintMemory(const unsigned char memory[],
                     const char label[] = "contents")
    {
      cout << "Memory " << label << ": " << endl;
      for (size_t i = 0; i < 4; i++) 
      {
        for (size_t j = 0; j < 8; j++)
          cout << setw(2) << setfill('0') << uppercase << hex
               << static_cast<int> (memory[i * 8 + j]) << " ";
        cout << endl;
      }
    }
    
    int main()
    {
      unsigned char memory[32];
      memset(memory, 0x11, 32 * sizeof(unsigned char));
      PrintMemory(memory, "before placement new");
    
      new (memory) A;
      PrintMemory(memory, "after placement new");
      reinterpret_cast<A *>(memory)->~A();
    
      system("pause");
      return 0;
    };

    Несмотря на относительно большой объём кода, логика его работы должна быть достаточно очевидна: на стеке выделяется 32 байта, которые заполняются значениями 0x11 (считаем, что это как бы «мусор» в памяти). Затем поверх этих 32 байт при помощи оператора placement new создаётся достаточно тривиальный объект класса A. Наконец, производится печать содержимого памяти, после чего программа разрушает объект и завершает своё выполнение. Ниже представлен вывод данной программы (Microsoft Visual Studio 2012, x64).
    Memory before placement new:
    11 11 11 11 11 11 11 11
    11 11 11 11 11 11 11 11
    11 11 11 11 11 11 11 11
    11 11 11 11 11 11 11 11
    ++ A has been constructed
    Memory after placement new:
    AA AA AA AA AA AA AA AA
    11 11 11 11 11 11 11 11
    11 11 11 11 11 11 11 11
    11 11 11 11 11 11 11 11
    -- A has been destructed
    Press any key to continue . . .

    Нетрудно заметить, что размер класса в памяти составляет 8 байт и равен размеру единственного его члена unsigned long long content_A.

    Немного усложним программу, добавив к объявлению функции void function(void) ключевое слово virtual:
    virtual void function(void) {nop();};

    Вывод программы (здесь и далее будет показываться лишь часть вывода за исключением Memory before placement new и Press any key...):
    ++ A has been constructed
    Memory after placement new:
    F8 D1 C4 3F 01 00 00 00
    AA AA AA AA AA AA AA AA
    11 11 11 11 11 11 11 11
    11 11 11 11 11 11 11 11
    -- A has been destructed

    Опять же, нетрудно заметить, что размер класса в памяти теперь составляет 16 байт. Первые восемь байт теперь занимает указатель на таблицу виртуальных методов. Указатель при этом запуске программы оказался равен 0x000000013FC4D1F8 (указатель и content_A «развёрнуты» в памяти, так как Intel64 использует little-endian порядок байт; правда, в случае с content_A так сразу и не скажешь об этом).

    Таблица виртуальных методов – специальная структура в памяти, генерируемая автоматически, в которой перечислены указатели на виртуальные методы. В случае, если где-то в коде вызывается метод function() применительно к указателю на класс A, вместо вызова непосредственно функции A::function() будет произведён вызов функции, находящейся в таблице виртуальных методов по нужному смещению – это поведение реализует полиморфизм. Сама по себе таблица виртуальных функций представлена ниже (получена путём компиляции с ключом /FAs; дополнительно обратите внимание на несколько странное имя функции в ассемблерном коде – оно прошло через "манглинг имён"):
    CONST SEGMENT
    ??_7A@@6B@ DQ  FLAT:??_R4A@@6B@   ; A::'vftable'
     DQ FLAT:?function@A@@UEAAXXZ
    CONST ENDS


    __declspec(novtable)


    Иногда бывают такие ситуации, когда таблица виртуальных классов, в принципе, не нужна. Предположим, что мы никогда не будем инстанцировать класс A, а если и будем, то только по выходным и в праздники, но при этом тщательно следя, чтобы не вызывалась ни одна виртуальная функция. Это достаточно частая ситуация в случаях абстрактных классов – известно, что если класс абстрактный, то он не может быть инстанцирован. Вообще никак. Действительно, если бы функция function(void) была бы объявлена в классе A как абстрактная, то таблица виртуальных методов выглядела бы следующим образом:
    CONST SEGMENT
    ??_7A@@6B@ DQ FLAT:??_R4A@@6B@ ; A::'vftable'
     DQ FLAT:_purecall
    CONST ENDS

    Очевидно, что попытка вызова такой функции приведёт к прострелу собственной ноги.

    Встаёт вопрос: если класс никогда не инстанцируется, то зачем устанавливать виртуальный табличный указатель? Для того, чтобы компилятор не генерировал лишний код, ему можно дать указание в виде __declspec(novtable) (осторожно: Microsoft-specific!). Перепишем наш пример класса с виртуальной функции с использованием атрибута __declspec(novtable):
    class __declspec(novtable) A { .... }

    Вывод программы станет следующим:
    ++ A has been constructed
    Memory after placement new:
    11 11 11 11 11 11 11 11
    AA AA AA AA AA AA AA AA
    11 11 11 11 11 11 11 11
    11 11 11 11 11 11 11 11
    -- A has been destructed

    В первую очередь обратим внимание на то, что размер объекта не изменился: он по-прежнему занимает 16 байт. Итого после внесения атрибута __declspec(novtable) появилось всего два отличия: во-первых, теперь на том месте, где раньше располагался адрес таблицы виртуальных методов, находится неинициализированная область памяти; во-вторых – в ассемблерном коде таблицы виртуальных методов класса A теперь нет вообще. Но виртуальный табличный указатель по-прежнему есть и по-прежнему «весит» восемь байт! Это нужно помнить, потому что…

    Наследование


    Перепишем наш пример таким образом, чтобы реализовать простейшее наследование от абстрактного класса с виртуальным табличным указателем.
    class __declspec(novtable) A // Я никогда не инстанцируюсь
    {
    public:
      unsigned long long content_A;
      A(void) : content_A(0xAAAAAAAAAAAAAAAAull)
          { cout << "++ A has been constructed" << endl;};
      ~A(void) 
          { cout << "-- A has been destructed" << endl;};
    
      virtual void function(void) = 0;
    };
    
    class B : public A // Я всегда инстанцируюсь вместо A
    {
    public:
      unsigned long long content_B;
      B(void) : content_B(0xBBBBBBBBBBBBBBBBull)
          { cout << "++ B has been constructed" << endl;};
      ~B(void) 
          { cout << "-- B has been destructed" << endl;};
    
      virtual void function(void) { nop(); };
    };

    Также сделаем так, чтобы вместо класса A в основной программе создавался (и уничтожался) класс B:
    ....
    new (memory) B;
    PrintMemory(memory, "after placement new");
    reinterpret_cast<B *>(memory)->~B();
    ....

    Вывод программы будет следующим:
    ++ A has been constructed
    ++ B has been constructed
    Memory after placement new:
    D8 CA 2C 3F 01 00 00 00
    AA AA AA AA AA AA AA AA
    BB BB BB BB BB BB BB BB
    11 11 11 11 11 11 11 11
    -- B has been destructed
    -- A has been destructed

    Попробуем разобраться, что произошло. Был вызван конструктор B::B(). Этот конструктор, прежде чем выполняться, вызывает конструктор базового класса, конструктор A::A(). В первую очередь тот должен был бы инициализировать виртуальный табличный указатель, однако из-за атрибута __declspec(novtable) он не был инициализирован. Затем конструктор устанавливает значение поля content_A в 0xAAAAAAAAAAAAAAAAull (второе поле в памяти) и возвращает управление конструктору B::B().

    Поскольку объект B не имеет атрибута __declspec(novtable), конструктор устанавливает виртуальный табличный указатель (первое поле в памяти) на таблицу виртуальных методов класса B, а затем устанавливает content_B в 0xBBBBBBBBBBBBBBBBull (третье поле в памяти) и возвращает управление основной программе. По содержимому памяти можно без труда понять, что объект класса B был сконструирован правильно, а из логики ясно, что ненужная в данном контексте операция была пропущена. Если запутались: под ненужной операцией имеется в виду инициализация указателя на виртуальную таблицу в конструкторе базового класса.

    Казалось бы, пропущена всего одна операция – смысл избавляться от неё? Но если в программе тысячи и тысячи классов, унаследованные от одного и того же абстрактного класса, избавление от одной автогенерируемой команды может серьёзно повлиять на производительность. И повлияет. Не верите?

    Функция memset


    Основная идея функции memset() – заполнение области памяти некоторым константным значением (чаще всего нулями). В языке Си её можно было использовать для быстрой инициализации всех полей структуры. А чем отличается класс С++ от структуры Си по расположению в памяти, если в нём нет виртуального табличного указателя? В принципе, ничем, данные – они и есть данные. Для инициализации действительно простых классов (в терминологии С++11 – типов со стандартным устройством) вполне возможно применять функцию memset(). Но, по идее, функцию memset() можно применять для инициализации вообще всех классов, вот только каковы будут последствия? Неправильный memset() может одним махом привести виртуальный табличный указатель в негодность. Но тут же встаёт вопрос: а, может, всё-таки можно, если класс объявлен как __declspec(novtable)?

    Ответ: можно, но только осторожно.

    Перепишем классы следующим образом: добавим метод wipe, который будет устанавливать всё содержимое класса A в 0xAA:
    class __declspec(novtable) A // Я никогда не инстанцируюсь
    {
    public:
      unsigned long long content_A;
      A(void)
        {
          cout << "++ A has been constructed" << endl;
          wipe();
        };
        // { cout << "++ A has been constructed" << endl; };
      ~A(void) 
        { cout << "-- A has been destructed" << endl;};
    
      virtual void function(void) = 0;
      void wipe(void)
      {
        memset(this, 0xAA, sizeof(*this));
        cout << "++ A has been wiped" << endl;
      };
    };
    
    class B : public A // Я всегда инстанцируюсь вместо A
    {
    public:
      unsigned long long content_B;
      B(void) : content_B(0xBBBBBBBBBBBBBBBBull)
          { cout << "++ B has been constructed" << endl;};
          // {
          //   cout << "++ B has been constructed" << endl;
          //   A::wipe();
          // };
    
      ~B(void) 
          { cout << "-- B has been destructed" << endl;};
    
      virtual void function(void) {nop();};
    };

    Вывод программы в этом случае получится достаточно ожидаемым:
    ++ A has been constructed
    ++ A has been wiped
    ++ B has been constructed
    Memory after placement new:
    E8 CA E8 3F 01 00 00 00
    AA AA AA AA AA AA AA AA
    BB BB BB BB BB BB BB BB
    11 11 11 11 11 11 11 11
    -- B has been destructed
    -- A has been destructed

    Пока всё работает хорошо.

    Однако стоит слегка изменить место вызова функции wipe(), закомментировав строки конструкторов и раскомментировав идущие за ними, и сразу станет ясно, что что-то пошло не так. Первый же вызов виртуальной функции function() обернётся ошибкой времени выполнения из-за повреждённого виртуального табличного указателя:
    ++ A has been constructed
    ++ B has been constructed
    ++ A has been wiped
    Memory after placement new:
    AA AA AA AA AA AA AA AA
    AA AA AA AA AA AA AA AA
    BB BB BB BB BB BB BB BB
    11 11 11 11 11 11 11 11
    -- B has been destructed
    -- A has been destructed

    Почему так произошло? Функция wipe() была вызвана уже после того, как конструктор класса B инициализировал указатель на таблицу виртуальных методов. В итоге этот указатель испортился. Иными словами – не стоит обнулять класс с виртуальным табличным указателем, даже если он объявлен с __declspec(novtable). Полное обнуление будет уместно только в конструкторе того класса, который никогда не будет инстанцирован, да и то делать это нужно с большой осторожностью.

    Функция memcpy


    С функцией memcpy() картина точно такая же. Опять же, по идее, ей можно пользоваться для того, чтобы копировать типы со стандартным устройством в памяти. Однако, если судить по практике, некоторые программисты любят пользоваться ей где надо и где не надо. В случае с типами, не обладающими стандартным устройством в памяти, использование функции memcpy() – это как хождение по канату над Ниагарским водопадом: одна ошибка может привести к фатальным последствиям, а совершить её до смешного просто. В качестве примера:
    class __declspec(novtable) A
    {
      ....
      A(const A &source) { memcpy(this, &source, sizeof(*this)); }
      virtual void foo() { }
      ....
    };
    class B : public A { .... };

    Конструктор копирования может писать всё, что его цифровой душе угодно, в указатель на виртуальную таблицу абстрактного класса: туда всё равно в классах-наследниках будет помещено правильное значение. А вот в реализации оператора присваивания использовать функцию memcpy() уже нельзя:
    class __declspec(novtable) A
    {
      ....
      A &operator =(const A &source)
      {
        memcpy(this, &source, sizeof(*this)); 
        return *this;
      }
      virtual void foo() { }
      ....
    };
    class B : public A { .... };

    А теперь вспомните, насколько мы привыкли, что оператор присваивания и конструктор копирования – это фактически одно и то же. Нет, не всё так плохо: на практике код оператора присваивания даже может исправно работать, но вовсе не потому, что он корректен, а потому, что так сложились звёзды. В коде копируется указатель на таблицу виртуальных методов из другого объекта, и во что это выльется – неизвестно.

    PVS-Studio


    Эта статья появилась как результат детального исследования касательно загадочного __declspec(novtable), а также когда можно, а когда нельзя использовать функции memset() и memcpy() в высокоуровневом коде. Нам время от времени пишут разработчики, что анализатор PVS-Studio слишком часто выдаёт предупреждения касательно виртуального табличного указателя. Программисты считают, что если есть __declspec(novtable), то нет ни таблицы виртуальных методов, ни виртуального табличного указателя. Мы начали внимательно разбираться с этим вопросом и поняли, что не всё так просто.

    Это надо запомнить. Если при объявлении класса используется __declspec(novtable), это не значит, что класс не содержит указателя на таблицу виртуальных методов! А вот инициализируется этот указатель или нет – это уже совсем другой вопрос.

    Мы сделаем так, чтобы анализатор не ругался на функции memset()/memcpy(), но только если они используются в конструкторах базового класса, объявленного с __declspec(novtable).

    Заключение


    К сожалению, в статье не удалось покрыть много материала, связанного с наследованием (к примеру, полностью непокрытой осталась тема множественного наследования). Однако надеюсь, что эта информация позволит понять, что «там не всё так просто» и что стоит три раза подумать, прежде чем использовать низкоуровневые функции применительно к высокоуровневым объектам. И вообще, стоит ли оно того?

    PVS-Studio

    516,00

    Ищем ошибки в C, C++ и C# на Windows, Linux, macOS

    Поделиться публикацией
    Комментарии 33
      +5
      C первого раза голова чуть не взорвалась. Потом вроде бы все понял, спасибо за статью.
        0
        Иной раз нам и не с такими штуками приходится разбираться :-).Так что не поделиться знаниями просто нельзя.
          0
          Кстати, для более глубокого понимания, очень рекомендую «Дизайн и эволюция С++» Страуструпа. Там описан и механизм, и то, что к такому решению привело.
            +1
            Ещё могу порекомендовать «C++ для настоящих программистов» (C++ for real programmers). Название книжки немного претензионное, но в ней хорошо описаны некоторые тонкости работы C++.
          +2
          И вообще, стоит ли оно того?

          Прочитал статью и решил, что, пожалуй, таки не стоит. Если уж сильно приспичит, лучше часть функционала на голых сях напишу, со всякой низкоуровневой магией.
            0
            Так, всё-же, в С++ нет методов, есть Member Functions, может подправите заголовок статьи?
              +1
              А в чём разница?
              +1
              Основная идея функции memset() – заполнение области памяти некоторым константным значением (чаще всего нулями). В языке Си её можно было использовать для быстрой инициализации всех полей структуры. А чем отличается класс С++ от структуры Си по расположению в памяти, если в нём нет виртуального табличного указателя? В принципе, ничем, данные – они и есть данные.


              memset`ом классы заливать?
              SuperPuperObj* mySuperPuperObj = new mySuperPuperObj;
              memset( mySuperPuperObj, 0, sizeof( SuperPuperObj ));
              

              ужос — я бы за такое убивал бы медленно и максимально учительно своих прогеров
                0
                Так никто и не говорил, что так надо делать :). А Ваш пример действительно ужас, так как memset вызывается уже после вызова конструктора. memset'ом можно инициализировать объекты, выделенные, например, с помощью malloc. Но опять таки этого никогда не стоит делать. В статье была просто проведена аналогия между структурами в Си и классами С++.
                  0
                  malloc — таки конечно да. Кто бы спорил.
                  Я так понял из абзаца автора что имеется ввиду именно занулить память выделенную под класс по указателю на него.

                  потому что даже структуру не стоит memset обнулять особенно если в ней есть члены с типом std::string или std::vector — гарантированно все плохо кончится
                  0
                  delete
                  поспешил, немного не о том написал…
                    0
                    Это везде и всюду. И часто меня убеждают, что это вообще чуть-ли не хороший и правильный стиль :). Ведь memset это быстро и понятно.
                      0
                      Ну не везде и не всюду. :-)
                      У нас в кампании такого нет.

                      Я лично когда то на заре своего взросления как спеца баловался такой штукенцией пока не словил багу: в классе который я мемсетил были std::vector и std::string члены.
                      И вот с далекого 1998го года я не балуюсь memset`ом для обнуления объектов и другим не рекомендую.
                      0
                      memset можно вызывать только в конструкторе, да и то только потому что конструктор знает все интимные детали класса. а снаружи — я с вами полностью согласен — нехорошо так делать
                      +2
                      Как-то все-таки в голове не укладывается — в век C++14, лямбда- шаблоно -виртуально- и пр.-измов использовать memset и особенно memcpy ?!?
                        0
                        Очень часто используют. Я постоянно вижу. Другое дело, что часто это работает, так как все члены класса простые. Но есть и: www.viva64.com/en/examples/V598/
                          0
                          Кстати, у вас там не все примеры ошибочны. Пример с Qt можно вычеркивать — memset вызывается еще до вызова конструктора, это совершенно корректно.
                            0
                            Можно, но оставлю. memset до конструткора… Само по себе уже Бррр…
                              +1
                              А что Бррр-то? POD-тип окажется забит нулями, как и планировалось, а у сложных типов потом корректно отработают конструкторы по умолчанию.

                              Для кода, который читает QML и преобразует его в объект — самое то.
                                +1
                                Ok. Удалю из базы.
                              0
                              (del)
                            0
                            а что в этом плохого?
                            если мне надо память выделить под данные и заполнить ее нулями?
                            Мне что класс писать с интерфейсами, виртуальными функциями и прочей С++ 14 фишками. Строчек этак на 5000 кода? :-)

                            Более того malloc`ом в некоторых случаях можно выделять память и через приведение типов от void* к SuperPuperObject* получить валидный указатель на объект
                            этим лучше не баловаться но реально можно получить незначительный профит перед вызовом new
                              +3
                              memset'ом строго говоря можно заполнять только trivial типы, и если вам нужны нули, то того же эффекта лучше добиться с помощью value-initialization (с возможной поправкой на наличие padding'a между полями).
                                0
                                возможной поправкой на наличие padding'a между полями

                                О да. Это зловредный «баг» :-)
                                И если #pragma pack(push,1) не указал, то можно долго искать «девушку в красном»
                            +2
                            Близко к этой теме: смешивание высокоуровневого и низкоуровневого — htrd.su/wiki/zhurnal/2013/09/18/zabavnyj_bag, так сказать, случай из личной практики.
                              0
                              Такая куча букв и слов, и всё для того, чтобы сказать: «дети, не стреляйте побайтово на не-POD-типах».

                              Даже без novtable может случиться беда: во время копирования-срезки произойдёт подмена vptr на адрес чужой vtable.
                              class A {
                                A& operator=(A const& src) { memcpy(this, &src, sizeof(A)); return *this; }
                                virtual void foo() {}
                              };
                              class B : public A {
                                ~B() {}
                                virtual void foo() {}
                                void bar() { foo(); }
                              };
                              class C : public A {
                                ~C() {}
                                virtual void foo() { buz(); }
                                void buz() {}
                              };
                              
                              int main() {
                                A a; B b; C c;
                                b = a; // теперь b думает, что он A (и стал ограниченно трудоспособен)
                                a = c; // теперь a думает, что он C (и может подёргать за несуществующие члены)
                                (A&)b = c; // теперь b думает, что он C
                              }
                              

                                +2
                                Это понятно. Началось всё с того, что пишут люди и просят устранить ложные срабатывания в анализаторе, так как они делают memcpy()/memset() для классов c __declspec(novtable). Мол ничего в них испортить нельзя. Это статья, так сказать, ответ им.
                                  0
                                  Так здесь ложных срабатываний вообще 0%.
                                  Потому что если полиморфный класс без vtable, он заведомо абстрактный по своей сути, и будет унаследован — со всеми вытекающими отсюда следствиями.

                                  Другое дело, что __declspec(novtable) не заставляет компилятор даже варнинг кинуть при попытке создать экземпляр такого класса — как это было бы, если бы у него были объявлены чисто виртуальные функции. (Я считаю, это дефект компилятора).
                                  Это даёт пользователям ложную уверенность.

                                  Кстати, ловит ли PVS Studio вот такое:
                                  struct __declspec(novtable) A
                                  {
                                  	virtual void foo() { printf("%p A::foo\n", this); }
                                  	void bar() { A::foo(); }
                                  	void buz() { foo(); }
                                  };
                                  
                                  int main()
                                  {
                                  	A a; // ошибка, на самом деле, вот здесь. "фурсенко разрешило"
                                  
                                  	a.foo(); // компилятор ***схитрил и оптимизировал*** динамический вызов на прямой
                                  	a.bar(); // компилятору явно приказали использовать прямой вызов
                                  	a.buz(); // гарантированный pure virtual function call, только ***недиагностируемый***!
                                  }
                                  

                                  В этом коде a.foo() и a.buz() — это UB, причём мы даже не попадём в обработчик-ловушку PVFC (указателями на который заполнены недостающие позиции таблиц виртуальных функций абстрактных классов).
                                    +2
                                    Так здесь ложных срабатываний вообще 0%.
                                    Или я Вас не понял, или Вы меня. Имеет место следующая ситуация. Люди пишут в поддержку и говорят:

                                    Анализатор PVS-Studio врёт, поправьте. У меня класс __declspec(novtable) и именно поэтому я его копирую/обнуляю, используя memcpy()/memset(). Это безопасно.

                                    Мы даже им в начале поверили (не один ведь человек пиcал про это). Но прежде чем что-то править, мы разбираемся с ситуацией. И выясняется, что не всё так просто. И что можно испортить указатель. Про это и статья. Впредь она будет служить ответом на такие письма.

                                    Кстати, ловит ли PVS Studio вот такое.
                                    Нет. Быть может добавим.
                                      0
                                      Если б я был султан… я бы отвечал таким отважным: «пофиг на novtable, вы копируете/обнуляете vptr суперкласса — в пункт назначения может попасть неожиданный мусор».

                                      В принципе, здоровое использование бинарно копируемых абстрактных классов — это когда типы источника и приёмника суть один и тот же класс-наследник. В этом случае паника на memcpy — это ложное срабатывание.
                                      Но, чтобы диагностировать ложность, надо выполнить покрытие кода и убедиться, что в конкретном месте типы действительно совпадают.

                                      А код в статье связан с созданием объектов абстрактного класса, что само по себе UB (хотя, по-хорошему, должно было быть ill-formed). К сожалению, именно это не диагностируется компилятором.
                                      В MSDN на этот счёт есть отписка
                                      If you attempt to instantiate a class marked with novtable and then access a class member, you will receive an access violation (AV).

                                0
                                Вы как-то странно развернули байты в памяти: меняется порядок байтов, порядок битов внутри байтов не меняется.
                                  0
                                  Ничего странного. Байт — это минимальная единица адресации памяти, поэтому порядок битов в нем зависит исключительно от того, каким алгоритмом мы его на эти биты раскладываем. Вопрос, как «на самом деле» эти биты хранятся в регистрах процессора и в ОЗУ, бессмысленный — мы все равно не можем работать с ними на этом уровне.

                                  А вот порядок байтов в слове (а также двойном слове и четверном слове) зависит от архитектуры процессора напрямую. И этот порядок на x86 и x64 — обратный, младший байт идет первым.
                                    0
                                    В статье была ошибка. Сейчас ее исправили.

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

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