О безопасном приведении типов
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* преобразовывать оно разумеется не хочет, не компилится. Выкрутился следующим образом, благо у меня был базовый класс:
XXX * p = NULL;
try
{
p = dynamic_cast<XXX>(t);
}
catch(...) {}
if (p)
{
// all ok...
}
Абсолютно не представляю насколько это идеологически правильно, переносимо или хорошо, скорее всего неправильно, криво и непереносимо:). Но у меня работает, почему dymamic_cast кидает исключение вместо того что бы вернуть NULL, я правда не понимаю, но практика (Visual Studio 2008) показывает что так и есть.
Тестировалось передачей 0 в качестве wParam и заменой первой строчки на
XXXBase * t = reinterpret_cast<XXXBase>(tt);
Все ок – исключение ловится, замалчивается, указатель остается нулевой.
August 3rd, 2009 at 3:24
А по-моему, архитектурная проблема случилось где-то раньше и в другом месте 🙂 Не мог бы ты привести постановку задачи, тип сообщения и причину, по которой всё сделано именно так?
August 3rd, 2009 at 4:08
Есть окно.
Есть не визуальный класс backend(X) который знает про окно и на нем рисует…
Окно просто создает X и говорит ему – рисуй.
<...>
Через какое-то время оказалось что нельзя просто сказать X рисуй и он начнет рисовать. Оказалось X’у понадобился еще один класс(Y) и теперь когда мы говорим X.рисуй – X вместо начала рисования должен сказать Y.хочу_рисовать, потом дождаться сигнала от У что все ок и только после этого работать.
Y знает что есть самое главное окно на котором нужно рисовать и имеет его хендл. У дает сигнал Х о том что можно рисовать – посредством посылки сообщения YYY окну(!). Но окно ничего не знает не про У не про сообщение YYY.
Поэтому для того что бы X узнал о том что пришло сообщение – была придумана такая схема – Х. Подменяет оконную процедуру окна, и ловит там свое сообщение. Наша новая оконная процедура – статический метод, соответственно она к классу Х мы обратиться не может, никаким другим способом кроме как, получить указатель на него в параметрах сообщение (wParam).
Вот собственно примерно так:)
Все что было после <...> – я к этому особого отношения не имею и почему сделано так не ведаю. Моя работа была подменить оконную процедуру и реализовать ее тело.
August 3rd, 2009 at 5:38
Сделай специальное зарегистрированное сообщение, а то пулять сообщение общего назначения, а в окне на авось кастовать параметры – ой как нехорошо. Даже WM_USER+XXX и то стремновато было бы.
August 4th, 2009 at 1:32
Там как раз и есть WM_USER+XXX, но поскольку программа большая, пишется кучей народу… хз, кто эти XXX еще использует.
+XXX это в основном варианте. А еще мне приходится пулять туда системное сообщение, но оно приходит только top-level windows, так что по идее не страшно.
А почему на авось кастовать? как раз таки вот мой код проверки прекрасно справляется с проверкой…
August 4th, 2009 at 1:34
Что-то с первого раза не получилось оставить сообщение. Я так и не понял почему всё-таки не
XXXBase * t = reinterpret_cast(wParam);
XXX * p = NULL;
p = dynamic_cast(t);
if (p)
{
// all ok…
}
August 4th, 2009 at 1:35
теперь понятно – движок блога теплэетный параметр XXX* сожрал
August 4th, 2009 at 1:42
Потому что dynamic_cast(t); бросает исключение… и надо бы его ловить.
August 4th, 2009 at 7:43
int(4) — это, извиняюсь, не проверка. Я бы всё таки зарегистрировал отдельное сообщение. А ещё лучше было бы эту странную конструкцию из двух классов, окна и сообщения перепроектировать 🙂
August 4th, 2009 at 12:25
Вместо 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).
August 4th, 2009 at 2:43
Учитывая, что в дело идёт пределах одной системы – не обязательно глобальная… Можно создавать в окне и протягивать WM_XCLASS в источник сообщения. Как-то так:
окно(хранит и регистрирует WM_XCLASS)->X(WM_XCLASS передаётся при создании из окна)->Y(WM_XCLASS передаётся при создании из X)
August 4th, 2009 at 2:58
Да вообще можно без всех этих сложностей.
RegisterWindowMessage() в ответ на одну и ту же строку вернёт одно и то же число.
August 4th, 2009 at 3:20
int * tt = new int(4);
XXXBase * t = reinterpret_cast(tt);
Поздравляю, ваш try/catch ловил падение программы. dynamic_cast предназначается только для преобразования объектов вверх и вниз по иерархии наследования. Т.е. если у вас указатель tt указывает на любое непонятное место, то dynamic_cast предсказуемо отработает только в 1 случае – если в непонятное место содержит данный класс. Во всех остальных случаях поведение неопределено. Поэтому представленный код неправильный.
August 4th, 2009 at 3:27
Konstantin, окно у нас ничего не должно хранить\делать… внутренние проблемы конкретного бекенда его абсолютно не волнуют….
Alexey, да именно поэтому я с начала и кастю указатель к базовому классу, и потому dynamic_cast проверяет уже … по крайней мере так задумывалось.
August 4th, 2009 at 3:50
Видишь ли в чем дело. dynamic_cast правильно работает только если вы ему действительно даете указатель на реальный класс. В вашем примере вы ему даете указатель на число, которое вы пытаетесь представить как класс, но реального класса там все равно нету. Поэтому если в wParam лежит не указатель на реальный класс (причем из той же иерархии), то программа упадет (или произойдет любое другое действие, подразумеваемое неопределенным поведением).
August 4th, 2009 at 3:54
Забыл. Еще правильно dynamic_cast отработает, если в wParam лежит 0 🙂
August 4th, 2009 at 4:16
А что только у меня ощущение что static функция и все вытекающие танцы с передачей класса и проверкой – дурная идея ?
August 4th, 2009 at 4:57
Ну оконную процедуру никакой другой бы ты и не подменил.
А вообще, я же сразу сказал – криво! Надо перепроектировать 🙂
P.S. Было бы окно твоим – можно было бы указатель ему в GWLP_USERDATA зашпиндорить.
August 4th, 2009 at 5:51
Все Win32 API так работает, примеров в MSDN навалом, так что это нормально
+1, но, к сожалению, не все разработчики одинаково умны, поэтому чем-то приходится жертвовать %)
2Begemot Все ж подумай по поводу регистрации сообщения
2worm2 Глобальные переменные это настолько плохо, особенно в больших системах, что просто слов нету. В этом плане например C# рулит абсолютно.
2Ippi а смысл? Это избавит от кастования? Что в юзердате, что в WPARAM, все равно надо кастить.
—
Вобще обсуждение ИМХО, такое себе. В данном случае есть базовый класс, разные наследники которого компилируются под разными платформами и внутри наследников используется native-код, который об остальных подсистемах, которые написаны на wxWidgets, ничего не знает. Под маком например вобще часть кода на Objective-C а сама прога на С++. У кого есть идеи как сконектить без динамик кастов и танцев с бубном, можем обсудить %)
August 4th, 2009 at 5:59
Я в том смысле, что это не хуже (а чем-то и лучше (например, быстрее)), чем зарегистрированное сообщение, когда есть уверенность, что GWLP_USERDATA больше никто не модифицирует (злонамеренную модификацию не рассматриваем – вариант с сообщением к ней так же не устойчив).
August 5th, 2009 at 1:27
В смысле подменить оконную процедуру функцией членом нельзя??
Тогда надо из класса делать singelton. Ну или как вариант просто указатель на класс хранить еще в одной статической переменной в классе..:) Таким образом можно избавится от необходимости передавать указатель на класс в параметрах сообщения.
А свое зареганное сообщение не спасет от ошибки программиста использующего наш код, который забудет передать ссылку в параметре – так что проверять все равно желательное:)
August 5th, 2009 at 10:28
Мудрость восторжествовала, спасибо Ippi 🙂
Сунули указатель на класс в GWLP_USERDATA, избавились от необходимости слать и кастовать параметр, и заодно от пробем со вторым инстансом класса.
August 5th, 2009 at 12:14
Только теперь большими красными буквами на главной форме напишите: “GWLP_USERDATA в таком-то окне НЕ ТРОГАТЬ!” 😀
August 5th, 2009 at 12:29
На самом деле, раз тебя такой подход устраивает, есть более безопасный, но чуть более медленный и почему-то редко используемый механизм – SetProp()/GetProp(). Только не забудь при уничтожении окна убивать все добавленные тобой к окну свойства с помощью RemoveProp() (лучше всего в обработчике WM_NCDESTROY).
August 5th, 2009 at 5:49
Хм… А почему окно не должно хранить идентификатор сообщения вызывающего отрисовку? ПММ, как раз логично в окне хранить идентификатор сообщения, который вызовет его “перерисовку” и не беспокоится кто там является источником запроса на перерисовку. По крайней мере это ближе к принципам MVC чем описаный изначально вариант, когда все знают о всех.
окно(хранит и регистрирует WM_XCLASS)->X(WM_XCLASS передаётся при создании из окна)->Y(WM_XCLASS передаётся при создании из X)
при необходимости сделать запрос на перерисовку шлём его через PostMessage(WM_XCLASS).
С помощью PostMessage(WM_XCLASS) у нас будет организован разрыв связи и зависимости окно->X->Y->окно в окно->X->Y-|разрыв|->окно.
GWLP_USERDATA такого разрыва не обеспечит точно.
August 15th, 2009 at 2:00
Чёт не пойму а чем не устраивает старый добрый (XXX *)wParam ?
>А свое зареганное сообщение не спасет от ошибки программиста использующего наш код, который забудет передать ссылку в параметре – так что проверять все равно желательное:)
Фигасе забудет. Лечить таких программистов надо, а не подстраиваться под них.
August 17th, 2009 at 3:30
Отсутствием проверки и вылетом если вдруго кто-то или что-то еще пришлет нам WM_USER + 0x01
Хотя сейчас уже перешли на ::RegisterWindowMessage()
August 17th, 2009 at 5:41
Мне кажется что “кто-то” или “что-то” недопустимы в разработке проекта любого уровня. Тем более вы даже не пытаетесь заявить об ошибке, а тихо её скипаете. Всё это обернётся тем что ошибка таки всплывёт в куда более печальной форме (неправильные вычисления например).
Кстати, тоже не панацея. С таким подоходом этот “кто-то” или “что-то” пошлёт сообщение с кодом из диапазона напрямую, без RegisterWindowMessage.
August 21st, 2009 at 6:51
Ужас, потом удивиляются почему сыпится прога на некоторых системах.