Правильное и неправильное использование динамической




информации о типе Динамическая информация о типе может использоваться во многихситуациях, в том числе для: объектного ввода-вывода,объектно-ориентированных баз данных, отладки. В тоже времявелика вероятность ошибочного использования такой информации.Известно,что в языке Симула использование таких средств,как правило, приводит к ошибкам. Поэтому эти средства не быливключены в С++. Слишком велик соблазн воспользоваться динамическойинформацией о типе, тогда как правильнее вызвать виртуальнуюфункцию. Рассмотрим в качестве примера класс Shape из $$1.2.5.Функцию rotate можно было задать так: void rotate(const Shape& s) // неправильное использование динамической // информации о типе { if (ref_type_info(s)==static_type_info(Circle)) { // для этой фигуры ничего не надо } else if (ref_type_info(s)==static_type_info(Triangle)) { // вращение треугольника } else if (ref_type_info(s)==static_type_info(Square)) { // вращение квадрата } //... } Если для переключателя по типу поля мы используем динамическуюинформацию о типе, то тем самым нарушаем в программе принципмодульности и отрицаем сами цели объектно-ориентированного программирования.К тому же это решение чревато ошибками: если в качествепараметра функции будет передан объект производного от Circle класса,то она сработает неверно (действительно, вращать круг (Circle)нет смысла, но для объекта, представляющего производный класс, этоможет потребоваться). Опыт показывает, что программистам, воспитаннымна таких языках как С или Паскаль, трудно избежать этой ловушки.Стиль программирования этих языков требует меньше предусмотрительности,а при создании библиотеки такой стиль можно просто считатьнебрежностью. Может возникнуть вопрос, почему в интерфейс с системой динамическойинформации о типе включена условная операция приведения ptr_cast(), а неоперация is_base(), которая непосредственно определяется с помощьюоперации has_base() из класса Type_info. Рассмотрим такой пример: void f(dialog_box& db) { if (is_base(&db,dbox_w_str)) { // является ли db базовым // для dbox_w-str? dbox_w_str* dbws = (dbox_w_str*) &db; //... } //... } Решение с помощью ptr_cast ($$13.5) более короткое, к тому же здесьявная и безусловная операция приведения отделена от проверки в оператореif, значит появляется возможность ошибки, неэффективности и даженеверного результата. Неверный результат может возникнуть в техредких случаях, когда система динамической идентификации типараспознает, что один тип является производным от другого, нотранслятору этот факт неизвестен, например: class D; class B; void g(B* pb) { if (is_base(pb,D)) { D* pb = (D*)pb; //... } //... } Если транслятору пока неизвестно следующее описание класса D: class D: public A, public B { //... }; то возникает ошибка, т.к. правильное приведение указателя pb к D*требует изменения значения указателя. Решение с операцией ptr_cast()не сталкивается с этой трудностью, поскольку эта операция примениматолько при условии, что в области видимости находятся описанияобеих ее параметров. Приведенный пример показывает, что операцияприведения для неописанных классов по сути своей ненадежна, нозапрещение ее существенно ухудшает совместимость с языком С.

Обширный интерфейс

Когда обсуждались абстрактные типы ($$13.3) и узловые классы ($$13.4),было подчеркнуто, что все функции базового класса реализуютсяв самом базовом или в производном классе. Но существует и другойспособ построения классов. Рассмотрим, например, списки, массивы,ассоциативные массивы, деревья и т.д. Естественно желание для всехэтих типов, часто называемых контейнерами, создать обобщающий ихкласс, который можно использовать в качестве интерфейса с любымиз перечисленных типов. Очевидно, что пользователь не должензнать детали, касающиеся конкретного контейнера. Но задачаопределения интерфейса для обобщенного контейнера нетривиальна.Предположим, что такой контейнер будет определен как абстрактныйтип, тогда какие операции он должен предоставлять? Можно предоставитьтолько те операции, которые есть в каждом контейнере, т.е.пересечение множеств операций, но такой интерфейс будет слишкомузким. На самом деле, во многих, имеющих смысл случаях такоепересечение пусто. В качестве альтернативного решения можнопредоставить объединение всех множеств операций и предусмотретьдинамическую ошибку, когда в этом интерфейсе к объектуприменяется "несуществующая" операция. Объединение интерфейсов классов,представляющих множество понятий, называется обширным интерфейсом.Опишем "общий" контейнер объектов типа T: class container { public: struct Bad_operation { // класс особых ситуаций const char* p; Bad_operation(const char* pp): p(pp) { } }; virtual void put(const T*) { throw Bad_operation("container::put"); } virtual T* get() { throw Bad_operation("container::get"); } virtual T*& operator[](int) { throw Bad_operation("container::[](int)"); } virtual T*& operator[](const char*) { throw Bad_operation("container::[](char*)"); } //... }; Все-таки существует мало реализаций, где удачно представлены какиндексирование, так и операции типа списочных, и, возможно, не стоитсовмещать их в одном классе. Отметим такое различие: для гарантии проверки на этапетрансляции в абстрактном типе используются чистые виртуальныефункции, а для обнаружения ошибок на этапе выполнения используютсяфункции обширного интерфейса, запускающие особые ситуации. Можно следующим образом описать контейнер, реализованныйкак простой список с односторонней связью: class slist_container: public container, private slist { public: void put(const T*); T* get(); T*& operator[](int) { throw Bad_operation("slist::[](int)"); } T*& operator[](const* char) { throw Bad_operation("slist::[](char*)"); } //... }; Чтобы упростить обработку динамических ошибок для спискавведены операции индексирования. Можно было не вводить этинереализованные для списка операции и ограничиться менее полнойинформацией, которую предоставляют особые ситуации, запущенныев классе container: class vector_container: public container, private vector { public: T*& operator[](int); T*& operator[](const char*); //... }; Если быть осторожным, то все работает нормально: void f() { slist_container sc; vector_container vc; //... } void user(container& c1, container& c2) { T* p1 = c1.get(); T* p2 = c2[3]; // нельзя использовать c2.get() или c1[3] //... } Все же для избежания ошибок при выполнении программы часто приходитсяиспользовать динамическую информацию о типе ($$13.5) или особыеситуации ($$9). Приведем пример: void user2(container& c1, container& c2) /* обнаружение ошибки просто, восстановление - трудная задача */ { try { T* p1 = c1.get(); T* p2 = c2[3]; //... } catch(container::Bad_operation& bad) { // Приехали! // А что теперь делать? } } или другой пример: void user3(container& c1, container& c2) /* обнаружение ошибки непросто, а восстановление по прежнему трудная задача */ { slist* sl = ptr_cast(slist_container,&c1); vector* v = ptr_cast(vector_container, &c2); if (sl && v) { T* p1 = c1.get(); T* p2 = c2[3]; //... } else { // Приехали! // А что теперь делать? } } Оба способа обнаружения ошибки, показанные на этих примерах,приводят к программе с "раздутым" кодом и низкой скоростьювыполнения. Поэтому обычно просто игнорируют возможные ошибкив надежде, что пользователь на них не натолкнется. Но задача от этогоне упрощается, ведь полное тестирование затруднительно и требуетмногих усилий. Поэтому, если целью является программа с хорошими характеристиками,или требуются высокие гарантии корректности программы, или, вообще,есть хорошая альтернатива, лучше не использовать обширные интерфейсы.Кроме того, использование обширного интерфейса нарушаетвзаимнооднозначное соответствие между классами и понятиями, и тогданачинают вводить новые производные классы просто для удобствареализации.


Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2016-02-16 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: