Вы все еще кипятите и сравниваете this с нулем?

    Давным-давно в далекой-далекой галактике широко использовалась библиотека MFC, в которой у ряда классов были методы, сравнивающие this с нулем. Примерно так:

    class CWindow {
        HWND handle;
        HWND GetSafeHandle() const
        {
             return this == 0 ? 0 : handle;
        }
    };
    

    «Это же не имеет смысла» – возразит читатель. Еще как «имеет»: этот код «позволяет» вызывать метод GetSafeHandle() через нулевой указатель CWindow*. Такой прием время от времени используется в разных проектах. Рассмотрим, почему на самом деле это плохая идея.

    Нужно начать с того, что, согласно Стандарту C++ (следует из 5.2.5/3 стандарта ISO/IEC 14882:2003(E)), вызов любого нестатического метода любого класса через нулевой указатель приводит к неопределенному поведению. Тем не менее, в ряде реализаций вот такой код вполне может работать:
    class Class {
    public:
        void DontAccessMembers()
        {
            ::Sleep(0);
        }
    };
    
    int main()
    {
        Class* object = 0;
        object->DontAccessMembers();
    }
    

    Это происходит благодаря тому, что во время работы метода нет попыток получить доступ к членам класса, а для вызова метода не используется позднее связывание. Компилятор знает, какой именно метод какого именно класса нужно вызвать, и просто добавляет вызов этого метода. При этом this передается как параметр. Эффект тот же, как если бы метод был статическим:
    class Class {
    public:
        static void DontAccessMembers(Class* currentObject)
        {
            ::Sleep(0);
        }
    };
    
    int main()
    {
        Class* object = 0;
        Class::DontAccessMembers(object);
    }
    

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

    Но мы же знаем, что наш метод никогда не будет вызываться виртуально, правда? И вообще этот код уже сколько-то там лет работает.

    Проблема в том, что компилятор может использовать неопределенное поведение для оптимизации. Вот например:
    int divideBy = …;
    whatever = 3 / divideBy;
    if( divideBy == 0 ) {
        // THIS IS IMPOSSIBLE
    }
    

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

    Точно так же компилятор может оптимизировать и код, сравнивающий this с нулем. В соответствии со Стандартом, this не может быть нулевым, соответственно, проверки и соответствующие ветви кода можно исключить, а это существенно повлияет на код, зависящий от сравнения this с нулем. Компилятор имеет полное право «сломать» (на самом деле — доломать) код CWindow::GetSafeHandle() и сгенерировать машинный код, в котором сравнения нет, а всегда считывается поле класса.

    Пока даже самые новые версии распространенных компиляторов (можно проверить с помощью сервиса GCC Explorer) не выполняют таких оптимизаций, так что пока «все работает», правда же?

    Во-первых, НЕНАВИСТЬ вы будете очень недовольны, когда после перехода на другой компилятор или другую версию того же компилятора вы потратите немало времени, чтобы обнаружить, что о, теперь такая оптимизация есть. Поэтому код выше является непереносимым.

    Во-вторых,
    class FirstBase {
        int firstBaseData;
    };
    
    class SecondBase {
    public:
        void Method()
        {
            if( this == 0 ) {
                printf( "this == 0");
            } else {
                printf( "this != 0 (value: %p)", this );
            }
        }
    };
    
    class Composed1 : public FirstBase, public SecondBase {
    };
    
    int main()
    {
        Composed1* object = 0;
        object->Method();
    }
    

    НУ НАДО ЖЕ, при компиляции на Visual C++ 9 указатель this на входе в метод равен 0x00000004, потому что изначально нулевой указатель корректируется так, чтобы указывать на начало подобъекта соответствующего класса.

    А если поменять порядок следования базовых классов
    class Composed2 : public SecondBase, public FirstBase {
    };
        
    int main()
    {
        Composed2* object = 0;
        object->Method();
    }
    

    при тех же самых условиях this будет нулевым, потому что начало подобъекта совпадает с началом объекта, в который он включен. Получается замечательный класс, метод которого работает только при условии «правильного» использования этого класса в составных объектах. Счастливой отладки, премия Дарвина давно не была так близко.

    Нетрудно заметить, что в случае класса Composed1 неявное преобразование указателя на объект к указателю на подобъект работает «неправильно» – для нулевого указателя на объект преобразование дает ненулевой указатель на подобъект. Обычно при реализации такого же по смыслу преобразования компилятор добавляет проверку указателя на равенство нулю. Например, компиляция вот такого кода с неопределенным поведением (класс Composed1 тот же, что выше):

    SecondBase* object = reinterpret_cast<Composed1*>( rand() );
    object->Method();
    

    на Visual C++ 9 дает такой машинный код:
    SecondBase* object = reinterpret_cast<Composed1*>( rand() );
    010C1000  call        dword ptr [__imp__rand (10C209Ch)] 
    010C1006  test        eax,eax
    010C1008  je          wmain+0Fh (10C100Fh) 
    010C100A  add         eax,4 
    object->Method();
    010C100D  jne         wmain+20h (10C1020h) 
    010C100F  push        offset string "this == 0" (10C20F4h) 
    010C1014  call        dword ptr [__imp__printf (10C20A4h)] 
    010C101A  add         esp,4 
    

    В этом машинном коде вторая инструкция – это сравнение указателя на объект с нулем, при равенстве указателя нулю управление не проходит через инструкцию add eax,4, которая сдвигает указатель. Здесь неявное преобразование реализовано с проверкой, хотя тоже можно было воспользоваться последующим вызовом метода через указатель и считать указатель ненулевым.

    В первом случае (вызов метода класса подобъекта прямо через указатель на объект класса) равенство приводимого указателя нулю тоже соответствует неопределенному поведению, и здесь проверка не добавляется. Если при чтении абзаца про оптимизацию кода с вызовом метода и последующей проверкой указателя на ноль, вы подумали, что это бред и фантастика, – напрасно, вот выше описан случай, в котором такая оптимизация по сути уже применена.

    Полагаться на вызов нестатического метода через нулевой указатель – плохая идея. Если необходима возможность выполнять метод для нулевого указателя, нужно сделать метод статическим, а указатель на объект явно передавать как параметр.

    Дмитрий Мещеряков,
    департамент продуктов для разработчиков
    ABBYY
    113.19
    Решения для интеллектуальной обработки информации
    Share post

    Comments 43

      +19
      Все правильно, но статью можно сократить до: «Придерживайтесь стандартов».
        +43
        О боги, о чем речь — если вы пришли к необходимости написать код проверяющий this на ноль, вы до этого что-то сделали не так.
          +5
          Don't use MFC :)
            +4
            Это еще что, я один раз, когда встретился с MFC и его указателями, долго думал, можно ли писать delete this. Так оказывается, что можно :)
              +1
              del
                +2
                Очень нерепрезентативная ссылка, при всём уважении к Алёне.
                В общем случае неизвестно, где ещё есть указатели на this, а они могут быть в том числе в сторонних либах, и что конкретно делает деструктор, который в случае MFC является обёрткой над win32 api, и, вполне вероятно, закрывает нижележащий хэндл, который опять же может висеть неизвестно где.
                Ну да, да, плохой дизайн и всё такое, но не стоит забывать, что на момент разработки MFC с одной стороны было системное api на С, к которому нужно было обеспечить прямой (читай, быстрый) доступ из С++, с другой — стандартная библиотека С++ пребывала в зачаточном и убогом в смысле поддержки состоянии, а с третьей — Майерс, Александреску и банда четырёх ещё только планировали свои книги.
              –7
              Хотите безопасный null — пишите на Objective C ;)
                +4
                Ага, одну проблему замени другой, вам не кажется не правильным формула — вызови метод из нулевого указателя и получи нулевой указатель в возвращемом значении? Особенно если ждеш дaлеко не этого.
                • UFO just landed and posted this here
                    0
                    Ну подумаешь парочка Memory Access + потенциальные кешмиссы. Процессор железный, не сгорит.
                    • UFO just landed and posted this here
                        0
                        Да, что-то не то написал. Перфомансно бесплатно.
                        Но проблемы с error handling остаются.
                      0
                      А если на выходе из функции ждем расчеты? Значение enum? Получаем сюрпризы с поведением :)
                      • UFO just landed and posted this here
                          0
                          Можно вполне сделать нулевым вариантом энамма как раз какой-нибудь InvalidResultState, тогда вызов метода у nil как раз вернет такое значение.
                        0
                        Ну я и не говорю что это идеальное решение. Единственный 100% верный вариант решения этой проблемы — отказаться от null вообще и использовать Maybe
                          +1
                          Чем раньше программа упадёт, тем лучше.
                          Если вызываем метод пустого объекта, которого не должно здесь быть, и метод не падает — это лишь отсрочка катастрофы.
                            0
                            Я с Вами согласен и поэтому у меня где надо расставлены ассерты, чтоб упасть в случае чего. То что падения из-за NullReferenceException помогают быстро задетектить баг — чистое совпадение.
                      –18
                      Ох уж эти аналитики сферических коней в вакууме.
                      Как красиво и логично рассуждать о правильном создании\удалении объектов на программах размеров в пару десятков классов, кушающих от силы пару десятков мегабайт ОЗУ. А вот что бывает на практике в настоящих проектах.

                      Есть такой себе движок для Javascript — V8. Используется в таких малоизвестных проектах как Google Chrome и Node.js, например. Так вот, менеджерение ресурсов и сборка мусора в проектах такого масштаба — штука сильно сложная, и на примитивной логике деструкторов её не реализовать — будет либо тормозить, либо глючить. И вот что же мы видим в главном header-файле v8.h?

                      virtual void Dispose() { delete this; }
                      


                      А знаете сколько всего раз delete this встречается в исходниках V8? 63 раза. О том, нафига оно так и каким образом там освобождаются ресурсы можно почитать тут.

                      А мысль в общем: «никогда не говорите „никогда“».
                        +15
                        Какая связь между использованием delete this и сравнением this с нулем?
                          +5
                          Да тоже, в общем-то, сомнительная практика.
                            +4
                            Если что, COM-ы с ихним IUnknown.Release() никуда не денутся от «delete this» внутри.
                            Попросить объект убиться — хороший способ удалить его, не разбираясь, в какой куче он расположен, и каким менеждером памяти создан.
                              0
                              Ну, виртуальные деструкторы для этого есть, вроде как.
                            +1
                            Может быть такая, что после вызова Dispose(), который сделает «delete this» у вас останется в наличии указатель, с которым дальше может произойти всё, что угодно (в том числе и вызов по нему следующего метода — а кто ж помешает?)
                              +4
                              Но и после обычного delete a; в указателе «a» такой же мусор.
                                +2
                                Ага, вот только delete a никого не ужасает, равно как и тот факт, что после этого его использовать не надо. А вот комментарий "О боги, о чем речь — если вы пришли к необходимости написать код проверяющий this на ноль, вы до этого что-то сделали не так" — вон выше +38 набрал, хотя суть то та же самая — ручное управление временем жизни ресурса и связанные с этим неудобства типа необходимости доп. проверок.
                            0
                            Помнится это уже обсуждали
                              0
                              Тоже узковато, «не нужно так, а если нужно — надо проверять». Об идее использования отдельного менеджера памяти, ответственного за сборку мусора в указанном ему множестве объектов, где он сам решает кого и как удалить не рассказала ни Алёна, ни вышеуказанная статья.
                            0
                            На самом деле, довольно печально, что все еще возникают вопросы, почему не нужно использовать оператор -> у нулевых указателей.
                              +4
                              С помощью этого можно узнавать смещение полей в структуре. Для этого есть макрос offsetof, который зачастую реализован именно через оператор -> у нулевого указателя.
                              #define offsetof(st, m) ((size_t)(&((st *)0)->m))
                            • UFO just landed and posted this here
                                +17
                                Чем больше знаешь C++ тем больше ты его не знаешь.
                                  +25
                                  Чем больше знаешь C++, тем больш Access violation reading location 0x00000004.
                                  +15
                                  Это стандартное состояние человека, пишущего на плюсах. Даже через несколько лет непрерывного хождения по граблям.
                                    0
                                    * Это стандартная суперпозиция знаний человека в области плюсов в произвольно взятый момент времени :)
                                      0
                                      Одно учишь, другое выскакивает?
                                        0
                                        Это когда знание есть но его при этом так же и нет, и это состояние постоянно, и не зависит от опыта.
                                      0
                                      Могу Вас успокоить — и через 20 лет это все равно не проходит :)
                                        0
                                        На самом деле, через несколько лет начинаешь ценить динамические языки для высокоуровневой логики и прототипирования, вынося в плюсы/чистый С только ресурсоёмкие либы.
                                        Самое главное в жизни программиста — это его затраченное время.
                                          0
                                          Сильно зависит от области. У меня (вычислительные задачи, высокопроизводительные системы, иногда мягкий real time) в большинстве случаев не получается. Хотя скриптовые языки (lua) использую широко — везде, где только возможно. Уже давно хотелось бы иметь что-то типа D, но когда оно еще дойдет до готовности к использованию в боевых системах. Да и с годами я как-то все больше начинаю ценить простоту, а в D с этим тоже не очень.
                                            +1
                                            «Мягкий реалтайм» это оксюморон. Реалтайм бывает либо жёсткий (когда не успел посчитать — значит отбрасывай), либо это не реалтайм. «Как бы надо уложиться в 40 мс» — это не пункт ТЗ, а строка художественного романа.
                                            Для вычислительных задач все низкоуровневые либы (читай решение СЛАУ, дифур, Фурье, вейвлеты и пр.) давно написаны и заоптимизированы производителями оборудования (intel,nvidia, etc). Разницы, вызывать их из С или из JS — нет. Если вы не можете свести свою задачу к стандартным — наймите математика. Если вы не можете прикрутить к своему проекту условный icc — наймите того, кто сможет.
                                            Высокопроизводительные системы — очень расплывчатое понятие «ни о чём», примерно как «мягкий реалтайм».
                                            В-общем, весь вопрос во взгляде архитектора и менеджера. Если в команде «только хардкор, только плюсы» и никто не считает деньги — то да, городим велосипеды. Если это космос на советских ТТЛШ или дешевый девайс на тупых копеечных SOIC — то тоже да, тоже городим велосипеды. В остальных случаях нет.
                                              +4
                                              Вы, батенька, совершенно не в теме.

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


                                              Мягкий риалтайм (fort realtime) — это общепринятый термин, наряду с другими видами. Ваши собственные верования тут имеют как бы мало ценности.

                                              Для вычислительных задач все низкоуровневые либы (читай решение СЛАУ, дифур, Фурье, вейвлеты и пр.) давно написаны и заоптимизированы производителями оборудования (intel,nvidia, etc).


                                              А это — широко распространенная легенда, в которую нравится верить тем, у кого с математикой проблемы. Для простейших применений действительно кое-что доступно, но я еще не встречал реальной задачи, в которой этого бы хватало. Кроме того есть масса математики, для которой никаких реализаций не доступно и масса случаев, когда под конкретную задачу разрабатываются совершенно новые математические методы. Нам тут больше всего нравится именно эта ниша, поскольку туда редко лезут «программисты», но и в обычных областях приходится подвизаться, когда эти «заоптимизированные» решения почему-то перестают справляться в конкретной задаче и приходится заменять их на «велосипед», который продолжает ехать.

                                              Если вы не можете свести свою задачу к стандартным — наймите математика. Если вы не можете прикрутить к своему проекту условный icc — наймите того, кто сможет.


                                              Типичная позиция программиста в худшем смысле этого слова. Мы тут как бы все математики (ага, т.н. «действующие», т.е. с фамилиями, узнаваемыми в сообществе математиков по публикациям) и все программируем профессионально. Как ни странно, такое быват, хотя и стоит очень дорого :)

                                              никто не считает деньги — то да, городим велосипеды


                                              Как раз опыт и умение считать деньги приводит к разработке кастомных решений, которые хорошо масштабируются в рамках той задачи, которая решается, а не под абстрактную «сферическую задачу в вакууме». А жертвы самоуверенных кодеров потом приходят со своими вроде бы работающими хреновинами и желанием, чтобы оно действительно работало и удивляются, почему сумма получается 6-значной. Но ничего, платят и получают бесценный опыт.
                                                0
                                                sed 's/fort/soft/'

                                  Only users with full accounts can post comments. Log in, please.