Главная страница
Навигация по странице:

  • 13.5 Динамическая информация о типе

  • 13.5.1 Информация о типе

  • 13.5.2 Класс Type_info

  • 13.5.3 Как создать систему динамических запросов о типе

  • Бьерн Страуструп. Язык программирования С Второе дополненное издание


    Скачать 2.87 Mb.
    НазваниеБьерн Страуструп. Язык программирования С Второе дополненное издание
    Дата30.01.2020
    Размер2.87 Mb.
    Формат файлаpdf
    Имя файлаStraustrup-Yazyk_programmirovaniya_c.pdf
    ТипДокументы
    #106559
    страница32 из 35
    1   ...   27   28   29   30   31   32   33   34   35

    13.4 Узловые классы
    В действительности иерархия классов строится, исходя из совсем другой концепции производных классов, чем концепция интерфейс-реализация, которая использовалась для абстрактных типов. Класс рассматривается как фундамент строения. Но даже, если в основании находится абстрактный класс, он допускает некоторое представление в программе и сам предоставляет для производных классов какие- то полезные функции. Примерами узловых классов могут служить классы rectangle ($$6.4.2) и satellite
    ($$6.5.1). Обычно в иерархии класс представляет некоторое общее понятие, а производные классы представляют конкретные варианты этого понятия. Узловой класс является неотъемлемой частью иерархии классов. Он пользуется сервисом, представляемым базовыми классами, сам обеспечивает определенный сервис и предоставляет виртуальные функции и (или) защищенный интерфейс, чтобы позволить дальнейшую детализацию своих операций в производных классах.
    Типичный узловой класс не только предоставляет реализацию интерфейса, задаваемого его базовым классом (как это делает класс реализации по отношению к абстрактному типу), но и сам расширяет интерфейс, добавляя новые функции. Рассмотрим в качестве примера класс dialog_box, который представляет окно некоторого вида на экране. В этом окне появляются вопросы пользователю и в нем он задает свой ответ с помощью нажатия клавиши или "мыши": class dialog_box : public window {
    // public: dialog_box(const char*
    ...);
    // заканчивающийся нулем список

    Бьерн Страуструп.
    Язык программирования С++
    343
    // обозначений клавиш
    // virtual int ask();
    };
    Здесь важную роль играет функция ask() и конструктор, с помощью которого программист указывает используемые клавиши и задает их числовые значения. Функция ask() изображает на экране окно и возвращает номер нажатой в ответ клавиши. Можно представить такой вариант использования: void user()
    { for (;;) {
    // какие-то команды dialog_box cont("continue",
    "try again",
    "abort",
    (char*)
    0); switch
    (cont.ask())
    { case
    0: return; case
    1: break; case
    2: abort();
    }
    }
    }
    Обратим внимание на использование конструктора. Конструктор, как правило, нужен для узлового класса и часто это нетривиальный конструктор. Этим узловые классы отличаются от абстрактных классов, для которых редко нужны конструкторы.
    Пользователь класса dialog_box ( а не только создатель этого класса) рассчитывает на сервис, представляемый его базовыми классами. В рассматриваемом примере предполагается, что существует некоторое стандартное размещение нового окна на экране. Если пользователь захочет управлять размещением окна, базовый для dialog_box класс window (окно) должен предоставлять такую возможность, например: dialog_box cont("continue","try again","abort",(char*)0); cont.move(some_point);
    Здесь функция движения окна move() рассчитывает на определенные функции базовых классов.
    Сам класс dialog_box является хорошим кандидатом для построения производных классов. Например, вполне разумно иметь такое окно, в котором, кроме нажатия клавиши или ввода с мышью, можно задавать строку символов (скажем, имя файла). Такое окно dbox_w_str строится как производный класс от простого окна dialog_box: class dbox_w_str : public dialog_box {
    // public: dbox_w_str
    ( const char* sl,
    // строка запроса пользователю const char*
    // список обозначений клавиш
    ); int ask(); virtual char* get_string();
    //...
    };
    Функция get_string() является той операцией, с помощью которой программист получает заданную пользователем строку. Функция ask() из класса dbox_w_str гарантирует, что строка введена правильно, а если пользователь не стал вводить строку, то тогда в программу возвращается соответствующее значение (0). void user2()

    Бьерн Страуструп.
    Язык программирования С++
    344
    {
    // dbox_w_str file_name("please enter file name",
    "done",
    (char*)0); file_name.ask(); char* p = file_name.get_string(); if (p) {
    // используем имя файла
    } else
    {
    // имя файла не задано
    }
    //
    }
    Подведем итог - узловой класс должен:
    [1] рассчитывать на свои базовые классы как для их реализации, так и для представления сервиса пользователям этих классов;
    [2] представлять более полный интерфейс (т.е. интерфейс с большим числом функций-членов) пользователям, чем базовые классы;
    [3] основывать в первую очередь (но не исключительно) свой общий интерфейс на виртуальных функциях;
    [4] зависеть от всех своих (прямых и косвенных) базовых классов;
    [5] иметь смысл только в контексте своих базовых классов;
    [6] служить базовым классом для построения производных классов;
    [7] воплощаться в объекте.
    Не все, но многие, узловые классы будут удовлетворять условиям 1, 2, 6 и 7. Класс, который не удовлетворяет условию 6, походит на конкретный тип и может быть назван конкретным узловым классом. Класс, который не удовлетворяет условию 7, походит на абстрактный тип и может быть назван абстрактным узловым классом. У многих узловых классов есть защищенные члены, чтобы предоставить для производных классов менее ограниченный интерфейс.
    Укажем на следствие условия 4: для трансляции своей программы пользователь узлового класса должен
    включить описания всех его прямых и косвенных базовых классов, а также описания всех тех классов, от
    которых, в свою очередь, зависят базовые классы. В этом узловой класс опять представляет контраст с
    абстрактным типом. Пользователь абстрактного типа не зависит от всех классов, использующихся для
    реализации типа и для трансляции своей программы не должен включать их описания.
    13.5 Динамическая информация о типе
    Иногда бывает полезно знать истинный тип объекта до его использования в каких-либо операциях.
    Рассмотрим функцию my(set&) из $$13.3. void my_set(set& s)
    { for ( T* p = s.first(); p; p = s.next()) {
    // мой код
    }
    //
    }
    Она хороша в общем случае, но представим,- стало известно, что многие параметры множества представляют собой объекты типа slist. Возможно также стал известен алгоритм перебора элементов, который значительно эффективнее для списков, чем для произвольных множеств. В результате эксперимента удалось выяснить, что именно этот перебор является узким местом в системе. Тогда,

    Бьерн Страуструп.
    Язык программирования С++
    345 конечно, имеет смысл учесть в программе отдельно вариант с slist. Допустив возможность определения истинного типа параметра, задающего множество, функцию my(set&) можно записать так: void my(set& s)
    { if (ref_type_info(s) == static_type_info(slist_set)) {
    // сравнение двух представлений типа
    // s типа slist slist& sl = (slist&)s; for (T* p = sl.first(); p; p = sl.next()) {
    // эффективный вариант в расчете на list
    }
    } else
    { for ( T* p = s.first(); p; p = s.next()) {
    // обычный вариант для произвольного множества
    }
    }
    //
    }
    Как только стал известен конкретный тип slist, стали доступны определенные операции со списками, и даже стала возможна реализация основных операций подстановкой.
    Приведенный вариант функции действует отлично, поскольку slist - это конкретный класс, и действительно имеет смысл отдельно разбирать вариант, когда параметр является slist_set.
    Рассмотрим теперь такую ситуацию, когда желательно отдельно разбирать вариант как для класса, так и для всех его производных классов. Допустим, мы имеем класс dialog_box из $$13.4 и хотим узнать, является ли он классом dbox_w_str. Поскольку может существовать много производных классов от dbox_w_str, простую проверку на совпадение с ним нельзя считать хорошим решением. Действительно, производные классы могут представлять самые разные варианты запроса строки. Например, один производный от dbox_w_str класс может предлагать пользователю варианты строк на выбор, другой может обеспечить поиск в каталоге и т.д. Значит, нужно проверять и на совпадение со всеми производными от dbox_w_str классами. Это так же типично для узловых классов, как проверка на вполне определенный тип типична для абстрактных классов, реализуемых конкретными типами. void f(dialog_box& db)
    { dbox_w_str* dbws = ptr_cast(dbox_w_str, &db); if (dbws) { // dbox_w_str
    // здесь можно использовать dbox_w_str::get_string()
    } else
    {
    //
    ``
    обычный'' dialog_box
    }
    //
    }
    Здесь "операция" приведения ptr_cast() свой второй параметр (указатель) приводит к своему первому параметру (типу) при условии, что указатель настроен на объект тип, которого совпадает с заданным
    (или является производным классом от заданного типа). Для проверки типа dialog_box используется указатель, чтобы после приведения его можно было сравнить с нулем.
    Возможно альтернативное решение с помощью ссылки на dialog_box: void g(dialog_box& db)
    { try
    { dbox_w_str& dbws = ref_cast(dialog_box,db);
    // здесь можно использовать dbox_w_str::get_string()
    } catch (Bad_cast) {

    Бьерн Страуструп.
    Язык программирования С++
    346
    //
    ``
    обычный'' dialog_box
    }
    //
    }
    Поскольку нет приемлемого представления нулевой ссылки, с которой можно сравнивать, используется особая ситуация, обозначающая ошибку приведения (т.е. случай, когда тип не есть dbox_w_str). Иногда лучше избегать сравнения с результатом приведения.
    Различие функций ref_cast() и ptr_cast() служит хорошей иллюстрацией различий между ссылками и указателями: ссылка обязательно ссылается на объект, тогда как указатель может и не ссылаться, поэтому для указателя часто нужна проверка.
    13.5.1 Информация о типе
    В С++ нет иного стандартного средства получения динамической информации о типе, кроме вызовов виртуальных функций. Хотя было сделано несколько предложений по расширению С++ в этом направлении.
    Смоделировать такое средство довольно просто и в большинстве больших библиотек есть возможности динамических запросов о типе. Здесь предлагается решение, обладающее тем полезным свойством, что объем информации о типе можно произвольно расширять. Его можно реализовать с помощью вызовов виртуальных функций, и оно может входить в расширенные реализации С++.
    Достаточно удобный интерфейс с любым средством, поставляющим информацию о типе, можно задать с помощью следующих операций: typeid static_type_info(type) // получить typeid для имени типа typeid ptr_type_info(pointer) // получить typeid для указателя typeid ref_type_info(reference) // получить typeid для ссылки pointer ptr_cast(type,pointer) // преобразование указателя reference ref_cast(type,reference) // преобразование ссылки
    Пользователь класса может обойтись этими операциями, а создатель класса должен предусмотреть в описаниях классов определенные "приспособления", чтобы согласовать операции с реализацией библиотеки.
    Большинство пользователей, которым вообще нужна динамическая идентификация типа, может ограничиться операциями приведения ptr_cast() и ref_cast(). Таким образом пользователь отстраняется от дальнейших сложностей, связанных с динамической идентификацией типа. Кроме того, ограниченное использование динамической информации о типе меньше всего чревато ошибками.
    Если недостаточно знать, что операция приведения прошла успешно, а нужен истинный тип (например, объектно-ориентированный ввод-вывод), то можно использовать операции динамических запросов о типе: static_type_info(), ptr_type_info() и ref_type_info(). Эти операции возвращают объект класса typeid.
    Как было показано в примере с set и slist_set, объекты класса typeid можно сравнивать. Для большинства задач этих сведений о классе typeid достаточно. Но для задач, которым нужна более полная информация о типе, в классе typeid есть функция get_type_info(): class typeid { friend class Type_info; private: const
    Type_info* id; public: typeid(const Type_info* p) : id(p) { } const Type_info* get_type_info() const { return id; } int operator==(typeid i) const ;
    };
    Функция get_type_info() возвращает указатель на неменяющийся (const) объект класса Type_info из typeid. Существенно, что объект не меняется: это должно гарантировать, что динамическая информация о типе отражает статические типы исходной программы. Плохо, если при выполнении программы некоторый тип может изменяться.

    Бьерн Страуструп.
    Язык программирования С++
    347
    С помощью указателя на объект класса Type_info пользователь получает доступ к информации о типе из typeid и, теперь его программа начинает зависеть от конкретной системы динамических запросов о типе и от структуры динамической информации о нем. Но эти средства не входят в стандарт языка, а задать их с помощью хорошо продуманных макроопределений непросто.
    13.5.2 Класс Type_info
    В классе Type_info есть минимальный объем информации для реализации операции ptr_cast(); его можно определить следующим образом: class Type_info { const char* n;
    // имя const Type_info** b;
    // список базовых классов public:
    Type_info(const char* name, const Type_info* base[]); const char* name() const;
    Base_iterator bases(int direct=0) const; int same(const Type_info* p) const; int has_base(const Type_info*, int direct=0) const; int can_cast(const Type_info* p) const; static const Type_info info_obj; virtual typeid get_info() const; static typeid info();
    };
    Две последние функции должны быть определены в каждом производном от Type_info классе.
    Пользователь не должен заботиться о структуре объекта Type_info, и она приведена здесь только для полноты изложения. Строка, содержащая имя типа, введена для того, чтобы дать возможность поиска информации в таблицах имен, например, в таблице отладчика. С помощью нее а также информации из объекта Type_info можно выдавать более осмысленные диагностические сообщения. Кроме того, если возникнет потребность иметь несколько объектов типа Type_info, то имя может служить уникальным ключом этих объектов. const char* Type_info::name() const
    { return n;
    } int Type_info::same(const Type_info* p) const
    { return this==p || strcmp(n,p->n)==0;
    } int Type_info::can_cast(const Type_info* p) const
    { return same(p) || p->has_base(this);
    }
    Доступ к информации о базовых классах обеспечивается функциями bases() и has_base(). Функция bases() возвращает итератор, который порождает указатели на базовые классы объектов Type_info, а с помощью функции has_base() можно определить является ли заданный класс базовым для другого класса. Эти функции имеют необязательный параметр direct, который показывает, следует ли рассматривать все базовые классы (direct=0), или только прямые базовые классы (direct=1). Наконец, как описано ниже, с помощью функций get_info() и info() можно получить динамическую информацию о типе для самого класса Type_info.
    Здесь средство динамических запросов о типе сознательно реализуется с помощью совсем простых классов. Так можно избежать привязки к определенной библиотеке. Реализация в расчете на конкретную библиотеку может быть иной. Можно, как всегда, посоветовать пользователям избегать излишней зависимости от деталей реализации.

    Бьерн Страуструп.
    Язык программирования С++
    348
    Функция has_base() ищет базовые классы с помощью имеющегося в Type_info списка базовых классов.
    Хранить информацию о том, является ли базовый класс частным или виртуальным, не нужно, поскольку все ошибки, связанные с ограничениями доступа или неоднозначностью, будут выявлены при трансляции. class base_iterator { short i; short alloc; const Type_info* b; public: const Type_info* operator() (); void reset() { i = 0; } base_iterator(const Type_info* bb, int direct=0);

    base_iterator() { if (alloc) delete[] (Type_info*)b; }
    };
    В следующем примере используется необязательный параметр для указания, следует ли рассматривать все базовые классы (direct==0) или только прямые базовые классы (direct==1). base_iterator::base_iterator(const Type_info* bb, int direct)
    { i = 0; if (direct) { // использование списка прямых базовых классов b
    = bb; alloc
    =
    0; return;
    }
    //
    создание списка прямых базовых классов:
    // int n = число базовых b = new const Type_info*[n+1];
    // занести базовые классы в b alloc = 1; return;
    } const Type_info* base_iterator::operator() ()
    { const Type_info* p = &b[i]; if (p) i++; return p;
    }
    Теперь можно задать операции запросов о типе с помощью макроопределений:
    #define static_type_info(T) T::info()
    #define ptr_type_info(p) ((p)->get_info())
    #define ref_type_info(r) ((r).get_info())
    #define ptr_cast(T,p) \
    (T::info()->can_cast((p)->get_info()) ? (T*)(p) : 0)
    #define ref_cast(T,r) \
    (T::info()->can_cast((r).get_info())
    \
    ? 0 : throw Bad_cast(T::info()->name()), (T&)(r))
    Предполагается, что тип особой ситуации Bad_cast (Ошибка_приведения) описан так: class Bad_cast { const char* tn;
    // public:
    Bad_cast(const char* p) : tn(p) { } const char* cast_to() { return tn; }
    // ...

    Бьерн Страуструп.
    Язык программирования С++
    349
    };
    В разделе $$4.7 было сказано, что появление макроопределений служит сигналом возникших проблем.
    Здесь проблема в том, что только транслятор имеет непосредственный доступ к литеральным типам, а макроопределения скрывают специфику реализации. По сути для хранения информации для динамических запросов о типах предназначена таблица виртуальных функций. Если реализация непосредственно поддерживает динамическую идентификацию типа, то рассматриваемые операции можно реализовать более естественно, эффективно и элегантно. В частности, очень просто реализовать функцию ptr_cast(), которая преобразует указатель на виртуальный базовый класс в указатель на его производные классы.
    13.5.3 Как создать систему динамических запросов о типе
    Здесь показано, как можно прямо реализовать динамические запросы о типе, когда в трансляторе таких возможностей нет. Это достаточно утомительная задача и можно пропустить этот раздел, так как в нем есть только детали конкретного решения.
    Классы set и slist_set из $$13.3 следует изменить так, чтобы с ними могли работать операции запросов о типе. Прежде всего, в базовый класс set нужно ввести функции-члены, которые используют операции запросов о типе: class set { public: static const Type_info info_obj; virtual typeid get_info() const; static typeid info();
    //
    };
    При выполнении программы единственным представителем объекта типа set является set::info_obj, который определяется так: const Type_info set::info_obj("set",0);
    С учетом этого определения функции тривиальны: typeid set::get_info() const { return &info_obj; } typeid set::info() { return &info_obj; } typeid slist_set::get_info() const { return &info_obj; } typeid slist_set::info() { return &info_obj; }
    Виртуальная функция get_info() будет предоставлять операции ref_type_info() и ptr_type_info(), а статическая функция info() - операцию static_type_info().
    При таком построении системы запросов о типе основная трудность на практике состоит в том, чтобы для каждого класса объект типа Type_info и две функции, возвращающие указатель на этот объект, определялись только один раз.
    Нужно несколько изменить класс slist_set: class slist_set : public set, private slist {
    // public: static const Type_info info_obj; virtual typeid get_info() const; static typeid info();
    //
    }; static const Type_info* slist_set_b[]
    = { &set::info_obj, &slist::info_obj, 0 }; const Type_info slist_set::info_obj("slist_set",slist_set_b); typeid slist_set::get_info() const { return &info_obj; } typeid slist_set::info() { return &info_obj; }

    Бьерн Страуструп.
    Язык программирования С++
    350
    1   ...   27   28   29   30   31   32   33   34   35


    написать администратору сайта