Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

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

Anna | 24.06.2014 | нет комментариев

Давным-давным-давно в дальней-дальней галактике обширно применялась библиотека 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 на входе в способ равен 0×00000004, потому что первоначально нулевой указатель корректируетсятак, Дабы указывать на предисловие подобъекта соответствующего класса.

А если поменять порядок следования базовых классов

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

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

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

Дмитрий Мещеряков,
департамент продуктов для разработчиков

 

Источник: programmingmaster.ru

 

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB