Pull to refresh
0
Content AI
Решения для интеллектуальной обработки информации

C++, приведение в стиле C и неожиданные последствия их сочетания

Reading time 5 min
Views 17K
C++ получил в наследство от C приведение вида (тип)(что привести) – обычно называется приведением в стиле C. В C++ есть еще четыре явных приведения – static_cast, reinterpret_cast, dynamic_cast, const_cast.

C++ – не самый новый язык, и жаркие споры о том, что лучше – приведение в стиле C или использование *_cast в нужном сочетании, начались давно и не утихают по сей день. Не будем подливать масла в огонь, лучше рассмотрим пример, и пусть каждый сам решит, что ему нравится больше.

Здесь будут упомянуты конструкции, специфичные для Windows и технологии COM, но такие же проблемы могут возникать в любых достаточно сложных иерархиях классов, если не уделять достаточно внимания приведению типов.

Пример по мотивам реального кода из реального проекта с открытым кодом. В некоторой подсистеме проекта объявлен класс, реализующий несколько COM-интерфейсов:


class CInterfacesImplementor : public IComInterface1, public IComInterface2,
    public IComInterface3, ...(еще интерфейсы с 4 по 9), public IComInterface10
{
     // определения методов всякие
};


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

Напомним, каждый COM-интерфейс прямо или опосредованно наследуется от IUnknown, а IUnknown содержит метод QueryInterface(), правильная реализация которого настолько непроста, что Raymond Chen написал об этом сериал (тут, тут и тут).

Наш пример – как раз реализация QueryInterface() в классе выше. Краткая предыстория: когда разработчик объявляет новый COM-интерфейс, он обязан назначить ему уникальный идентификатор. Вызывающая сторона вызывает QueryInterface() для того, чтобы узнать, реализует ли объект интерфейс с таким идентификатором и, если реализует, получить указатель соответствующего типа. Конструкция «__uuidof()» просит Visual C++ во время компиляции найти и подставить идентификатор интерфейса, указанного в скобках.

Итак…

    HRESULT STDMETHODCALLTYPE CInterfaceImplementor::QueryInterface( REFIID iid, void** ppv )
    {
        if( ppv == 0 ) {
            return E_POINTER;
        }
        if( iid == __uuidof( IUnknown ) || iid == __uuidof( IComInterface1 ) ) {
            *ppv = (IComInterface1*)this;
        } else if( iid == __uuidof( IComInterface2 ) ) {
            *ppv = (IComInterface2*)this;
        } else if( iid == __uuidof( IComInterface3 ) ) {
            *ppv = (IComInterface3*)this;
        } else if... 
        ... // то же самое для каждого реализованного COM-интерфейса
        } else { // никакие другие COM-интерфейсы этот класс не реализует
           *ppv = 0;
           return E_NOINTERFACE;
        }
        AddRef();
        return S_OK;
    }


Реализация выше работает и почти совершенна. Она проверяет указатель перед разыменованием. Она проверяет, известный ли интерфейс у нее запросили. Она записывает нулевой указатель перед возвратом кода E_NOINTERFACE. Она увеличивает счетчик ссылок, если интерфейс поддержан. Она даже на запрос IUnknown правильно реагирует. Raymond Chen был бы доволен, если бы не один вопрос.

Зачем там приведения? Почему не написать «*ppv = this;»?

При множественном наследовании объект будет «сложен» из подобъектов базовых классов так, чтобы можно было получить доступ к каждому подобъекту отдельно. Скажем, какая-то функция умеет работать только c IComInterface2* – нужно передать ей указатель именно на этот подобъект, а не на производный объект, о составе которого она, вполне возможно, ничего не знает.

Присваивание «*ppv = this;» привело бы к тому, что всякий раз передавался бы адрес начала производного объекта, а не подобъектов, из которых он состоит. Попытка вызвать виртуальный метод интерфейса через указатель на другой подобъект, очевидно, приведет к долгой отладке.

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

Счастье есть? До этого абзаца – точно. Теперь проходит 100500 дней, проект развивается, в него добавляется новая функциональность. В следующем абзаце мы увидим последствия неудачного применения копипаста при попытке развить проект. Только давайте обойдемся без возражений, что «правильные программисты» при «правильном программировании» и «правильной архитектуре» так якобы не делают.

В другой подсистеме того же проекта с открытым кодом есть другой класс, реализующий тот же набор интерфейсов:

    class CYetOtherImplementor : public IComInterface1,
        public IComInterface3, ...(еще интерфейсы с 4 по 9), public IComInterface10
    {
        // определения методов всякие
    };

и, естественно, писать ту цепь условий заново никому не хочется, тем более что реализация, очевидно, такая же:

    HRESULT STDMETHODCALLTYPE CYetOtherImplementor::QueryInterface( REFIID iid, void** ppv )
    {
        if( ppv == 0 ) {
            return E_POINTER;
        }
        if( iid == __uuidof( IUnknown ) || iid == __uuidof( IComInterface1 ) ) {
            *ppv = (IComInterface1*)this;
        } else if( iid == __uuidof( IComInterface2 ) ) {
            *ppv = (IComInterface2*)this;
        } else if( iid == __uuidof( IComInterface3 ) ) {
            *ppv = (IComInterface3*)this;
        } else if... 
        ... // то же самое для каждого реализованного COM-интерфейса
        } else { // никакие другие COM-интерфейсы этот класс не реализует
           *ppv = 0;
           return E_NOINTERFACE;
        }
        // V2UncmUgaGlyaW5nIC0gd3d3LmFiYnl5LnJ1L3ZhY2FuY3k=
        AddRef();
        return S_OK;
     }

А теперь мысленно проиграем, что произойдет при запросе интерфейса IComInterface2. Управление пойдет по цепи if-else-if до совпадения идентификатора, и затем будет выполнено приведение в стиле C.

Параграф 5.3.5/5 стандарта C++ ISO/IEC 14882:2003(E) говорит, что при приведении в стиле C будет выполнен (в нашем случае) либо static_cast, либо, если static_cast невозможен, – reinterpret_cast.

В первом примере класс был унаследован от IComInterface2 и выполнялся static_cast указателя this к указателю на нужный подобъект.

Во втором примере класс уже не унаследован от IComInterface2 (да, копипаст плюс доработка напильником), поэтому static_cast невозможен. Будет выполнен reinterpret_cast, указатель this будет скопирован без изменений. И кстати, объект вообще не реализует IComInterface2. Здесь уместно слово ВДРУГ.

Вызывающая сторона при запросе IComInterface2 во втором примере получит ненулевой указатель на объект, который этот интерфейс не реализует и вообще никак к этому интерфейсу не относится.

Для сравнения, если использовать static_cast в каждой из веток if-else-if, компилятор выдаст сообщение об ошибке и второй пример не скомпилируется, это мягко намекнет разработчику, что надо поработать напильником еще немного. Минус день отладки, можно заняться чем-нибудь полезным.

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

Можно предположить, что приведения в стиле C позволяют писать код короче, но усложняют написание правильного кода и только отодвигают момент, когда придется все же освоиться с приведениями *_cast.

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

Дмитрий Мещеряков
Департамент продуктов для ввода данных
Tags:
Hubs:
+40
Comments 67
Comments Comments 67

Articles

Information

Website
www.contentai.ru
Registered
Founded
Employees
101–200 employees
Location
Россия