|
Программирование >> Обобщенные обратные вызовы
Это что, ошибка компилятора? Следует ли программисту писать гневное письмо разработчику компилятора? Искать хитрый вирус, заползший из недр Интернета? Списать все на проблему 2000 года? Выписать шамана из дебрей Амазонки для изгнания злых духов из системного блока? Что же все-таки произошло? Решение 1. Один программист написал следующий код. [...] Что же все-таки произошло? Если говорить коротко - такие симптомы могут быть вызваны разными заболеваниями, но их совокупность с высокой степенью вероятности говорит о том, что профаммист наступил на грабли непродуманного использования макросов. Мотивация Некоторые распространенные среды профам м ирован ия С++ предоставляют макросы, преднамеренно созданные для изменения имен функций. Обычно это делается исходя из хороших или невинных побуждений, в частности - для обратной или прямой совместимости API. Например, если функция sleep в некоторой версии операционной системы заменена функцией sleepEx, производитель, поставляющий заголовочный файл, в котором объявлены эти функции, может решить любезно предоставить макроопределение, которое автоматически заменит имя sleep на SleepEx: #define sleep SleepEx Это - определенно Плохая Мысль. Макросы представляют собой антитезу инкапсуляции, поскольку их область действия невозможно проконфолировать - даже автору макроса. Макросам наплевать... Макросы по многим причинам - весьма неприятная вещь, которая может стать попросту опасной. В первую очередь это связано с тем, что макросы - средство замены текста, действующее во время обработки исходного текста препроцессором, до того как начнется какая-либо проверка синтаксиса и семантики. Далее перечислены некоторые из неприятностей, связанных с макросами. /. Макросы изменяют имена - слишком часто, чтобы это не приносило особого вреда. Будет преуменьшением сказать, что проблема только в том, что это мешает при отладке приложения. Такое переименование при помощи макросов означает, что когда вы думаете, что вызываете некоторую функцию, на самом деле ее вызов не происходит. Рассмотрим, например, нашу функцию sleep, не являющуюся членом. int sleepC Animal* а ) { return a->Sleep( 1 ); } Вы не в состоянии найти sleep ни в объектном коде, ни в карте, созданной компоновщиком, - просто потому что в действительности она называется SleepEx. Когда вы столкнетесь с отсутствием функции Sleep, вы можете решить, что компилятор встроил ее в код - это может объяснить, куда подевалась ваша функция и почему ее не видно в объектном коде. Если вы придете к такому заключению и напишете гневное письмо по поводу сверхагрессивиой оптимизации разработчику компилятора, окажется, что вы обратились не по адресу, не в ту компанию (или, про крайней мере, не в тот ее отдел). Некоторые из читателей книги вполне могли сталкиваться с такими неприятностями в своей практике. Если вы, как и я, не удовлетворитесь первым попавшимся объяснением таинственного исчезновения функции и начнете искать ее при помощи пошагового прохождения программы, то обнаружите, что в той строке, где должна вызываться ваша функция, действительно имеет место вызов функции, только какой-то другой, несмотря на то, что в вашем исходном тексте указано верное ее имя. Обычно такое поведение при пошаговом прохождении быстро направляет мысли в правильное русло, и вы понимаете, что же именно произошло, и не всегда тихим, но всегда недобрым словом поминаете автора .этого замечательного макроса. Но это еще не все. Дело в том, что: 1(6). С++ имеет собственные средства для работы с именами. То, что макросы предоставляют другой способ для выполнения той же работы, приводит к эффекту, который лучше всего определить как нездоровое взаимодействие . Вы можете решить, что не такое уж это и большое дело - изменение имени функции. Ну что ж, зачастую это действительно так. Но что если вы изменили имя функции на другое, принадлежащее существующей функции? Что делает С++, когда обнаруживает две функции с одинаковыми именами? Он перегружает их. А это совсем не так хорошо, тем более если это происходит так, что вы об этом и не подозреваете. Именно это, увы, и происходит в случае с нашей функцией sleep. Вся причина того, что разработчик библиотеки любезно предоставил макрос для автоматического переименования Sleep в SleepEx заключается в наличии обоих функций в поставляемой библиотеке. Рассмотрим случай, когда эти функции могут иметь различные сигнатуры. Когда мы пишем собственную функцию Si eep, мы знаем о наличии библиотечной функции Sleep и можем принять меры для того, чтобы избежать неоднозначностей или других ошибок, которые могут привести к проблемам. Более того, мы можем умышленно использовать перефузку, чтобы получить функцию с поведением, похожим на поведение библиотечной функции sleep. Но если при этом окажется, что имя функции совсем иное, то перегрузка не только поведет себя не так, как ожидалось, но ее может не быть вообще. В контексте нашего вопроса такой изменяющий имя функции sleep макрос может частично объяснить, почему в разных ситуациях вызываются разные функции; то, какая именно функция будет вызвана, может зависеть от того, как именно сработает разрешение перегрузки для конкретных типов, использованных в различных местах вызова. Иногда вызывается наша функция, иногда - библиотечная; а то, какая именно функция будет вызвана, зависит от многих обстоятельств, причем, возможно, не самым очевидным образом. Если бы даже на этом наш рассказ о последствиях применения макросов и закончился, рассказанное было бы достаточно неприятно. Но, к сожалению, осколки от взрыва макроса разлетаются в разных направлениях... 2. Макросы не заботятся о типах. Исходное предназначение макроса sleep, о котором шла речь выше, - изменение имени глобальной функции. К сожалению, макрос изменяет имя Sleep везде, где только находит его. Если ему попадется глобальная переменная с именем sleep, то это имя будет молча заменено на SleepEX. Это еще одна очень Плохая Вещь. 3. Макросы не заботятся об области видимости. Еще хуже то, что макрос, созданный для замены имени глобальной функции, не являющейся членом класса, будет с тем же успехом заменять как имена функций, так и другие такие же имена, являющиеся членами класса или инкапсулированные в ваше собственное просфанство имен. В рассматриваемом в задаче случае у нас имеется класс, в котором есть функции Sleep и SleepEx; многие из описанных в условии задачи проблем могут быть, по крайней мере, частично объяснены наличием макроса, п е ре н мс и о вьшаю ще го sleep, что приводит к невидимой перегрузке наших собственных функций друг с другом. Такая перегрузка функций-членов (как и глобальная, о которой рассказывалось в п. 1) может объяснить, почему иногда вызывается не та функция-член, которая ожидается. Это зависит от того, как именно будет выполнено разрешение перефузки для конкретных типов, использованных в точке вызова. Если вы решите, что это еше одна Плохая Вещь - вы окажетесь совершенно правы. Больше всего это напоминает доктора без перчаток и диплома (неразумный разработчик заголовочного файла библиотеки) с грязными руками (макросами), который вскрывает брюшную полость пациента (класс или пространство имен) и начинает там копаться, переставляя местами внутренности (члены и прочий код)... во время приступа лунатизма (даже не осознавая, что же он делает). Резюме Если говорить коротко - макросы никогда не заботятся ни о чем. > Рекомендация Избегайте макросов. Никогда, никогда, никогда даже не думайте о том, чтобы написать макрос, который представляет собой обычное слово или аббревиатуру. Ваша стандартная реакция на макрос должна быть однозначной - Изыди! (или, на худой конец, Vadc retro! ) - пока вы полностью не уясните себе причины его использования в конкретном случае, где его применение не является хаком . Макросы небезопасны с точки зрения типов, с точки зрения областей видимости... да они просто небезопасны и точка! Если вы вынуждены писать макрос - избегайте размещения его в заголовочном файле и попытайтесь дать ему длинное и персонифицированное имя, которое вряд ли кому-нибудь придет в голову использовать в качестве имени в программе (и вообще вряд ли придет в голову кому-либо). > Рекомендация Для инкапсуляции имен предпочтительно использовать пространства имен. Коротко говоря - пользуйтесь инкапсуляцией. Хорошая инкапсуляция - не только признак хорошего дизайна; это еще и возможная защита от неожиданных угроз, о которых вы можете даже не подозревать. Макросы представляют собой антитезу инкапсуляции в силу того, что область действия макроса не может конфолироваться даже его автором. Классы и пространства имен входят в число полезных инструментов С++, которые помогают управлять и минимизировать взаимозависимости между различными частями профаммы, которые по дизайну не должны быть связаны друг с другом. Благоразумное использование этих и других возможностей С++ для обеспечения инкапсуляции не только обеспечивает лучший дизайн, но и служит в то же время защитой от непродуманного кодирования колл е г- п рофам м и сто в.
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |