О безопасном приведении типов

August 3rd, 2009 Begemot Posted in Программирование

Семь раз отмерь – один отрежь (с)

Ну или вернее о той ерунде которую я сегодня написал.

Задача была такая: подменяем оконную процедуру, при получении определенного сообщения берем wParam, преобразуем к указателю на нужный класс и вызываем функцию член. Сразу представили ужасы, ждущие пользователя если нам придет это сообщение (откуда-нибудь), но без указателя на наш класс. А значит надо, как-то проверять. Как?

Первое что приходит в голову – dynamic_cast<>(), читаем что бы освежить запас мудрости:

Безопасное приведение по иерархии наследования, в том числе и для виртуального наследования.
dynamic_cast(base_class_ptr_expr)
Используется RTTI (Runtime Type Information), чтобы привести один указатель на объект класса к другому указателю на объект класса. Классы должны быть полиморфными, то есть в базовом классе должна быть хотя бы одна виртуальная функция. Если эти условие не соблюдено, ошибка возникнет на этапе компиляции. Если приведение невозможно, то об этом станет ясно только на этапе выполнения программы и будет возвращен NULL.
dynamic_cast(base_class_ref_expr)
Работа со ссылками происходит почти как с указателями, но в случае ошибки во время исполнения будет выброшено исключение bad_cast.

Спасибо Алёне, там же читаем и про остальные возможности приведения типов в c++.

dynamic_cast<>() приведение конечно безопасное, но wParam в XXX* преобразовывать оно разумеется не хочет, не компилится. Выкрутился следующим образом, благо у меня был базовый класс:

XXXBase  * t = reinterpret_cast<XXXBase*>(wParam);
XXX * p = NULL;
try
{
   p = dynamic_cast<XXX>(t);
}
catch(...) {}

if (p)
{
// all ok...
}

Абсолютно не представляю насколько это идеологически правильно, переносимо или хорошо, скорее всего неправильно, криво и непереносимо:). Но у меня работает, почему dymamic_cast кидает исключение вместо того что бы вернуть NULL, я правда не понимаю, но практика (Visual Studio 2008) показывает что так и есть.

Тестировалось передачей 0 в качестве wParam и заменой первой строчки на

int * tt = new int(4);
XXXBase  * t = reinterpret_cast<XXXBase>(tt);

Все ок – исключение ловится, замалчивается, указатель остается нулевой.

Related:

28 Responses to “О безопасном приведении типов”

  1. А по-моему, архитектурная проблема случилось где-то раньше и в другом месте 🙂 Не мог бы ты привести постановку задачи, тип сообщения и причину, по которой всё сделано именно так?

  2. Есть окно.
    Есть не визуальный класс backend(X) который знает про окно и на нем рисует…
    Окно просто создает X и говорит ему – рисуй.
    < ...>
    Через какое-то время оказалось что нельзя просто сказать X рисуй и он начнет рисовать. Оказалось X’у понадобился еще один класс(Y) и теперь когда мы говорим X.рисуй – X вместо начала рисования должен сказать Y.хочу_рисовать, потом дождаться сигнала от У что все ок и только после этого работать.

    Y знает что есть самое главное окно на котором нужно рисовать и имеет его хендл. У дает сигнал Х о том что можно рисовать – посредством посылки сообщения YYY окну(!). Но окно ничего не знает не про У не про сообщение YYY.

    Поэтому для того что бы X узнал о том что пришло сообщение – была придумана такая схема – Х. Подменяет оконную процедуру окна, и ловит там свое сообщение. Наша новая оконная процедура – статический метод, соответственно она к классу Х мы обратиться не может, никаким другим способом кроме как, получить указатель на него в параметрах сообщение (wParam).

    Вот собственно примерно так:)
    Все что было после < ...> – я к этому особого отношения не имею и почему сделано так не ведаю. Моя работа была подменить оконную процедуру и реализовать ее тело.

  3. Сделай специальное зарегистрированное сообщение, а то пулять сообщение общего назначения, а в окне на авось кастовать параметры – ой как нехорошо. Даже WM_USER+XXX и то стремновато было бы.

  4. Там как раз и есть WM_USER+XXX, но поскольку программа большая, пишется кучей народу… хз, кто эти XXX еще использует.
    +XXX это в основном варианте. А еще мне приходится пулять туда системное сообщение, но оно приходит только top-level windows, так что по идее не страшно.

    А почему на авось кастовать? как раз таки вот мой код проверки прекрасно справляется с проверкой…

  5. Что-то с первого раза не получилось оставить сообщение. Я так и не понял почему всё-таки не
    XXXBase * t = reinterpret_cast(wParam);
    XXX * p = NULL;
    p = dynamic_cast(t);
    if (p)
    {
    // all ok…
    }

  6. теперь понятно – движок блога теплэетный параметр XXX* сожрал

  7. Потому что dynamic_cast(t); бросает исключение… и надо бы его ловить.

  8. Begemot: Там как раз и есть WM_USER+XXX, но поскольку программа большая, пишется кучей народу… хз, кто эти XXX еще использует.+XXX это в основном варианте. А еще мне приходится пулять туда системное сообщение, но оно приходит только top-level windows, так что по идее не страшно.А почему на авось кастовать? как раз таки вот мой код проверкипрекрасно справляется с проверкой…

    int(4) — это, извиняюсь, не проверка. Я бы всё таки зарегистрировал отдельное сообщение. А ещё лучше было бы эту странную конструкцию из двух классов, окна и сообщения перепроектировать 🙂

  9. Вместо WM_USER можно зарегистрировать свой, уникальный для класса X и thread’а, в котором выполняется код, номер сообщения следующим образом:

    int WM_XCLASS; // глобальная переменная

    // инициализация приложения
    char XClassID[100];
    sprintf(XClassID, “XClassName %d”, GetCurrentThreadID());
    WM_XCLASS = RegisterWindowMessage(XClassID);

    Привязать же к Handl’у окна нужный нам указатель на экземпляр класса X можно с помощью функций WinAPI GlobalAddAtom() и SetProp() (см. справку по последней функции, например, в MSDN).

  10. Konstantin Says:

    int WM_XCLASS; // глобальная переменная

    // инициализация приложения
    char XClassID[100];
    sprintf(XClassID, “XClassName %d”, GetCurrentThreadID());
    WM_XCLASS = RegisterWindowMessage(XClassID);

    Учитывая, что в дело идёт пределах одной системы – не обязательно глобальная… Можно создавать в окне и протягивать WM_XCLASS в источник сообщения. Как-то так:
    окно(хранит и регистрирует WM_XCLASS)->X(WM_XCLASS передаётся при создании из окна)->Y(WM_XCLASS передаётся при создании из X)

  11. Konstantin:
    Учитывая, что в дело идёт пределах одной системы – не обязательно глобальная… Можно создавать в окне и протягивать WM_XCLASS в источник сообщения.

    Да вообще можно без всех этих сложностей.
    RegisterWindowMessage() в ответ на одну и ту же строку вернёт одно и то же число.

  12. int * tt = new int(4);
    XXXBase * t = reinterpret_cast(tt);

    Поздравляю, ваш try/catch ловил падение программы. dynamic_cast предназначается только для преобразования объектов вверх и вниз по иерархии наследования. Т.е. если у вас указатель tt указывает на любое непонятное место, то dynamic_cast предсказуемо отработает только в 1 случае – если в непонятное место содержит данный класс. Во всех остальных случаях поведение неопределено. Поэтому представленный код неправильный.

  13. Konstantin, окно у нас ничего не должно хранить\делать… внутренние проблемы конкретного бекенда его абсолютно не волнуют….
    Alexey, да именно поэтому я с начала и кастю указатель к базовому классу, и потому dynamic_cast проверяет уже … по крайней мере так задумывалось.

  14. Видишь ли в чем дело. dynamic_cast правильно работает только если вы ему действительно даете указатель на реальный класс. В вашем примере вы ему даете указатель на число, которое вы пытаетесь представить как класс, но реального класса там все равно нету. Поэтому если в wParam лежит не указатель на реальный класс (причем из той же иерархии), то программа упадет (или произойдет любое другое действие, подразумеваемое неопределенным поведением).

  15. Забыл. Еще правильно dynamic_cast отработает, если в wParam лежит 0 🙂

  16. А что только у меня ощущение что static функция и все вытекающие танцы с передачей класса и проверкой – дурная идея ?

  17. Begemot: А что только у меня ощущение что static функция и все вытекающие танцы с передачей класса и проверкой– дурная идея ?

    Ну оконную процедуру никакой другой бы ты и не подменил.
    А вообще, я же сразу сказал – криво! Надо перепроектировать 🙂
    P.S. Было бы окно твоим – можно было бы указатель ему в GWLP_USERDATA зашпиндорить.

  18. А по-моему, архитектурная проблема случилось где-то раньше и в другом месте 🙂

    Все Win32 API так работает, примеров в MSDN навалом, так что это нормально

    Сделай специальное зарегистрированное сообщение, а то пулять сообщение общего назначения, а в окне на авось кастовать параметры – ой как нехорошо.

    +1, но, к сожалению, не все разработчики одинаково умны, поэтому чем-то приходится жертвовать %)
    2Begemot Все ж подумай по поводу регистрации сообщения
    2worm2 Глобальные переменные это настолько плохо, особенно в больших системах, что просто слов нету. В этом плане например C# рулит абсолютно.
    2Ippi а смысл? Это избавит от кастования? Что в юзердате, что в WPARAM, все равно надо кастить.

    Вобще обсуждение ИМХО, такое себе. В данном случае есть базовый класс, разные наследники которого компилируются под разными платформами и внутри наследников используется native-код, который об остальных подсистемах, которые написаны на wxWidgets, ничего не знает. Под маком например вобще часть кода на Objective-C а сама прога на С++. У кого есть идеи как сконектить без динамик кастов и танцев с бубном, можем обсудить %)

  19. T-Rex: а смысл? Это избавит от кастования? Что в юзердате, что в WPARAM, все равно надо кастить.

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

  20. Ippi: Ну оконную процедуру никакой другой бы ты и не подменил.

    В смысле подменить оконную процедуру функцией членом нельзя??
    Тогда надо из класса делать singelton. Ну или как вариант просто указатель на класс хранить еще в одной статической переменной в классе..:) Таким образом можно избавится от необходимости передавать указатель на класс в параметрах сообщения.

    А свое зареганное сообщение не спасет от ошибки программиста использующего наш код, который забудет передать ссылку в параметре – так что проверять все равно желательное:)

  21. Мудрость восторжествовала, спасибо Ippi 🙂

    Сунули указатель на класс в GWLP_USERDATA, избавились от необходимости слать и кастовать параметр, и заодно от пробем со вторым инстансом класса.

  22. Begemot: Сунули указатель на класс в GWLP_USERDATA

    Только теперь большими красными буквами на главной форме напишите: “GWLP_USERDATA в таком-то окне НЕ ТРОГАТЬ!” 😀

  23. На самом деле, раз тебя такой подход устраивает, есть более безопасный, но чуть более медленный и почему-то редко используемый механизм – SetProp()/GetProp(). Только не забудь при уничтожении окна убивать все добавленные тобой к окну свойства с помощью RemoveProp() (лучше всего в обработчике WM_NCDESTROY).

  24. Konstantin Says:

    Begemot:
    Konstantin, окно у нас ничего не должно хранить\делать… внутренние проблемы конкретного бекенда его абсолютно не волнуют….

    Хм… А почему окно не должно хранить идентификатор сообщения вызывающего отрисовку? ПММ, как раз логично в окне хранить идентификатор сообщения, который вызовет его “перерисовку” и не беспокоится кто там является источником запроса на перерисовку. По крайней мере это ближе к принципам MVC чем описаный изначально вариант, когда все знают о всех.

    окно(хранит и регистрирует WM_XCLASS)->X(WM_XCLASS передаётся при создании из окна)->Y(WM_XCLASS передаётся при создании из X)
    при необходимости сделать запрос на перерисовку шлём его через PostMessage(WM_XCLASS).
    С помощью PostMessage(WM_XCLASS) у нас будет организован разрыв связи и зависимости окно->X->Y->окно в окно->X->Y-|разрыв|->окно.
    GWLP_USERDATA такого разрыва не обеспечит точно.

  25. Чёт не пойму а чем не устраивает старый добрый (XXX *)wParam ?

    >А свое зареганное сообщение не спасет от ошибки программиста использующего наш код, который забудет передать ссылку в параметре – так что проверять все равно желательное:)

    Фигасе забудет. Лечить таких программистов надо, а не подстраиваться под них.

  26. Dmitry: Чёт не пойму а чем не устраивает старый добрый (XXX *)wParam ?

    Отсутствием проверки и вылетом если вдруго кто-то или что-то еще пришлет нам WM_USER + 0x01

    Хотя сейчас уже перешли на ::RegisterWindowMessage()

  27. Begemot: Отсутствием проверки и вылетом если вдруго кто-то или что-то еще пришлет нам WM_USER + 0×01

    Мне кажется что “кто-то” или “что-то” недопустимы в разработке проекта любого уровня. Тем более вы даже не пытаетесь заявить об ошибке, а тихо её скипаете. Всё это обернётся тем что ошибка таки всплывёт в куда более печальной форме (неправильные вычисления например).

    Хотя сейчас уже перешли на ::RegisterWindowMessage()

    Кстати, тоже не панацея. С таким подоходом этот “кто-то” или “что-то” пошлёт сообщение с кодом из диапазона напрямую, без RegisterWindowMessage.

  28. Ужас, потом удивиляются почему сыпится прога на некоторых системах.