Использование typeid для простых решений - PullRequest
1 голос
/ 22 июня 2011

Мой вопрос касается простого отлова проблем приведения во время выполнения в C ++. Я понимаю, что в большинстве случаев C ++ не предоставляет «RTTI» (скажем так, я не могу изменить настройки компилятора и включить его, ради аргумента.).

В моем проекте у меня есть контейнер, предоставленный мне древней библиотекой. Давайте назовем это Фродо и контейнером Бэйна.

Бродяга Фродо - это контейнер, с которым я должен взаимодействовать напрямую. Он плохо реализован и содержит несколько карт одной и той же информации, которыми я должен управлять отдельно. Контейнер содержит несколько экземпляров определенного типа, назовите его Ring.

Для моей реализации я должен использовать подтип Ring, OneRing, для всех других операций. Это связано с внешними требованиями для моей конкретной реализации Бродяги Фродо.

Мне иногда приходится просматривать свои списки, и давайте просто скажем, что есть вещи, которые мне нужны от OneRing, которые я не могу получить ни от какого старого кольца.

Есть ШАНС, просто случайный ШАНС, что Фродо положит нормальное старое Кольцо в мою Бэйн.

Будучи слишком Pepsi gen, чтобы понять, что C ++ был неспособен ответить на простой вопрос, такой как «это кольцо также OneRing» или перехватить исключения, возникающие из-за того, что он принял кольцо за OneRing, я немного погуглил и нашел что есть такие вещи, как typeid и type_of, но есть множество причин не использовать каждый из них.

Итак, если ограничить проблему простым желанием избежать принятия Кольца за OneRing, есть ВСЕ, что я могу безопасно и эффективно сделать, БЕЗ возможности изменить реализацию Ring.

Спасибо, куча за любой совет!

ОБНОВЛЕНИЕ: Хорошо, ниже приведено много полезных советов, но после дня экспериментов я могу сказать следующее о RTTI в нативной Visual Studio 2008 C ++:

  • C ++ имеет RTTI только на указателях до классов . Вы не можете динамически разыграть void *, и если вы запросите информацию о типе, это будет void *. RTTI включен на указатель . Я понимаю, почему вы не «приводите» член (с безумной точки зрения C ++, в языках без членов вам не нужно делать такое искусственное различие), и, возможно, эти факты связаны по какой-то архаичной причине эффективности / сложности компилятора.
  • Если вы получите информацию о типе Ring * для OneRing, это будет Ring *. Почему они решили не помещать информацию на классы Я понятия не имею. Это делает typeid () практически бесполезным - он даже возвращает закрытый класс с единственным членом, к которому у вас есть доступ, именем char *! Это как 11/10 по шкале недружелюбия и бесполезности - они сделали это нарочно?
  • Вы можете безопасно свернуть дерево наследования с помощью динамического приведения, но опять же вы не сможете сделать это, если у вас нет и не известен общий предок наследования для любых данных, которые вы получаете.
  • Если вы выходите за рамки описанного выше сценария или у вас нет RTTI по ​​какой-либо причине, вы попадаете в поговорку, потому что нет способа поймать исключение или сгенерировать ошибку в результате неправильного приведения, если вы не реализуете какая-то форма RTTI самостоятельно.
  • Такие вещи, как COM, стремятся дать более полезный RTTI, предоставляя всем общую базу, на которой есть некоторая предварительно объявленная информация RTTI ... Я думаю. : -)

В качестве дополнительного примечания Microsoft заявляет, что (по крайней мере, с 2008 года) RTTI является частью C ++ и включен по умолчанию . Это невероятно сколько противоречивой информации по этому одному я нашел.

Это могут быть старые новости для тех из вас, кто провел большую часть вашего времени в Родной стране, но для меня это заняло сеанс экспериментов, немного поиска в Google, публикации в StackOverflow, дополнительного поиска в Google и экспериментов.

Спасибо всем, кто указал мне правильное направление, и, пожалуйста, поправьте меня, если вы все еще чувствуете, что я что-то пропустил. : -)

Ответы [ 2 ]

1 голос
/ 22 июня 2011

Если контейнер содержит экземпляры Ring, а не указатели на них, у вас не может быть реального OneRing, а только нарезанный кусочек, вставленный в Ring.В этом случае вы не можете точно знать, что это был за объект до того, как его вставили в контейнер, поскольку теперь это обычный Ring для каждой практической цели.

Если вместо этого контейнер хранит указатели /ссылки на элементы (как я полагаю), вы можете просто попробовать dynamic_cast из Ring указателя / ссылки, которую вы получаете на тип OneRing;если dynamic_cast завершается успешно, указатель / ссылка правильно приводятся к его «реальному» типу OneRing, в противном случае вы знаете, что это не OneRing, а просто обычный Ring (или какой-либо другой производный класс, который не 't OneRing).

Очевидно, dynamic_cast требует, чтобы RTTI работал правильно (если RTTI отключен, информация о типах не включается в исполняемый файл, поэтому ничего не известно о типах во время выполнения), поэтому не забудьте включить егоесли ваш компилятор отключает его по умолчанию.

Если вы не можете включить его, вы не можете использовать dynamic_cast или typeid;в языке нет встроенного «альтернативного» механизма, потому что он будет избыточным, и для правильной работы RTTI потребуется та же информация, что и для RTTI.

Единственное, что вы можете сделать, - это заново изобрести RTTI в своем классе.иерархия, обеспечивающая переопределенный метод virtual для возврата разных значений для каждого подкласса;таким образом, вы могли бы проверить из Ring указателя, что это за Ring, на самом деле, тогда вы могли бы жестоко разыграть его.Обратите внимание, что это громоздко, сложно поддерживать и небезопасно (приведение жестких указателей может не работать в сложных сценариях наследования), поэтому я настоятельно рекомендую не использовать этот метод.Просто используйте RTTI.

Кстати, имейте в виду, что во многих случаях, если вам нужен dynamic_cast, что-то не так в вашей иерархии классов: хорошо спроектированные иерархии классов стремятся достичь своих целей без динамического приведения,используя правильно virtual методы.

Мой вопрос касается простого отлова проблем приведения во время выполнения в C ++.Я понимаю, что C ++ не предоставляет «RTTI» в большинстве случаев (скажем так, я не могу изменить настройки своего компилятора и включить его, ради аргумента.).

Стандарт C ++ предоставляет RTTI,Вы можете отключить его в компиляторах , если он вам не нужен , чтобы сохранить некоторое исполняемое пространство, которое было бы потрачено впустую для ненужной информации о типе.Если вам вместо этого нужен RTTI, непонятно, почему вы хотите его отключить.

Слишком много Pepsi gen, чтобы понять, что C ++ не способен ответить на простой вопрос типа «это кольцо также OneRing?'или поймать исключения, возникающие из-за принятия Кольца за OneRing

Именно в этом и заключается цель dynamic_cast.Примените его к указателю, и если он не может быть преобразован, вы получите NULL, примените его к ссылке, и если он не может быть преобразован, выдается std::bad_cast.

Приложение

C ++ имеет RTTI только для указателей на классы.Вы не можете динамически разыграть void *, и если вы запросите информацию о типе, это будет void *.RTTI находится «на» указатель.Я понимаю, почему вы не «приводите» член (с безумной точки зрения C ++, в языках без членов вам не нужно делать такое искусственное различие), и, возможно, эти факты связаны по какой-то архаичной причине эффективности / сложности компилятора.

Подождите, подождите, это не работает так, информация RTTI не указатель, это как-то на объекте.Но чтобы понять, почему это такой беспорядок, я думаю, вам нужно немного информации о том, как этот материал работает под прикрытием.

Уведомление : всето, что следует, зависит от реализации, стандарт не требует какого-либо конкретного способа реализации виртуальных функций и RTTI, просто это обычно делается так.

В C ++ неполиморфный класс - это просто нормальная структура, содержащая его поля. Различие между частным и общедоступным вводится во время компиляции, а методы - это обычные функции, которые вызываются со скрытым параметром this, который указывает на экземпляр класса, для которого они вызываются. Каждый вызов метода разрешается во время компиляции. Наследование - это просто вставка полей базового класса перед добавленными полями. Все просто, все счастливы.

Если статический тип всегда совпадает с «реальным» типом объекта, все работает нормально. Проблемы начинают возникать, когда вы хотите иметь полиморфное поведение: если вы сохраняете Derived * в переменной-указателе, статический тип которой равен Base *, «статический» и «настоящий» типы больше не совпадают. Видя вызов, выполненный для такого указателя, компилятор не имеет ни малейшего представления о необходимости вызова метода Derived, а просто вызывает метод Base, поскольку вся информация, которую он имеет, является статическим типом такого указатель.

Чтобы решить эту проблему, были изобретены virtual вызовов и vtables. Когда вы объявляете метод класса как virtual, ваш класс становится полиморфным классом, то есть он допускает полиморфное поведение методов, объявленных как virtual.

Это работает так. Для каждого класса компилятор создает таблицу указателей на функции (так называемый «vtable»), которая помещается где-то в исполняемый образ; есть один «ряд» для каждого virtual метода. В vtable базового класса будут содержаться указатели на реализацию Base виртуальных методов, в то время как производные классы будут иметь свои vtable, содержащие указатели на их версии методов. Таблицы производных классов также могут быть больше из-за дополнительных методов, но методы, присутствующие также в базовом классе, находятся в начале, и индексы для общих методов в Derived vtable совпадают с индексами Base.

Теперь у каждого объекта полиморфного класса есть дополнительный член - указатель виртуальной таблицы (обычно называемый vptr, обычно помещаемый в начало класса). Этот элемент автоматически инициализируется непосредственно перед запуском каждого конструктора, чтобы указать на правильный vtable для объекта. Обратите внимание, что именно по этой причине при полиморфном типе запускается конструктор базового класса, виртуальные методы не работают «правильно» (т. Е. Они работают так, как если бы класс был типа Base): производный конструктор hasn ' пока не запущен, поэтому vptr по-прежнему указывает на Base vtable.

Когда объект полностью построен, его «реальный» тип теперь гораздо более определен, чем для неполиморфного объекта: если у вас есть Derived *, сохраненный в Base *, компилятор теперь сможет вызывать правильные версии виртуальных методов: каждый виртуальный вызов реализован как поиск в виртуальной таблице (к которой можно получить доступ, поскольку vptr присутствует как в Base, так и в Derived в той же позиции), после чего следует вызов функция, указатель которой хранится в правильном месте виртуальной таблицы (индекс для конкретной виртуальной функции одинаков в виртуальной таблице каждого производного класса). Поскольку вызываемая функция «знает», что она работает с экземпляром Derived, она может получить доступ ко всем членам класса Derived.

Это более или менее то, как виртуальные функции работают для одиночного наследования в «простой» полиморфной иерархии классов. При множественном наследовании или при смешивании полиморфных классов с неполиморфными все становится запутанным (у вас может быть несколько vptr, макет класса должен соответствовать некоторым ограничениям, ...), но это не относится к нам.

Теперь у нас работают виртуальные функции. typeid это всего лишь маленький шаг отсюда.

RTTI необходимо отслеживать некоторую информацию, связанную с «реальным» типом каждого объекта. Итак, поскольку у нас уже есть vtables, разумная идея - поместить такую ​​информацию (возможно, как указатель на большую структуру) где-нибудь в vtable - например, в начале или в конце. Таким образом, вам не нужно добавлять еще один скрытый указатель внутри каждого объекта - одного из vtable достаточно как для виртуальных вызовов, так и для информации о типе.

Каждый полиморфный класс будет иметь связанную структуру, содержащую информацию о его типе, которая может включать имя, некоторый уникальный идентификатор (может не потребоваться, если имя гарантировано будет уникальным) и, вероятно, указатели на информацию о типе базовых классов. (они понадобятся для dynamic_cast).

Когда вы вызываете typeid для класса, вы получаете экземпляр type_info, непрозрачный класс, который встраивает небольшую часть этой информации, а именно: имя класса и скрытый уникальный идентификатор (на самом деле, часто это будет содержать только одно из них: если компилятор достаточно дружественен для предоставления имени, он, вероятно, удостоверится, что он уникален, чтобы повторно использовать его также в качестве уникального идентификатора).

Элемент name практически бесполезен для целей без отладки. Настоящая полезность type_info - это operator==, которую он предоставляет; на самом деле, основная цель type_info состоит в том, что он может быть по сравнению , и два type_info объекта оцениваются равными тогда и только тогда, когда они являются результатом typeid для идентичных типов. Таким образом, основное использование typeid состоит в том, чтобы проверить, является ли реальный тип указателя / ссылки на объект точно таким же, как другой (или тип, фиксированный во время компиляции). Сравнение обычно выполняется путем сравнения уникального идентификатора (возможно, указателя на структуру RTTI в памяти) или уникального имени.

dynamic_cast - гораздо более сложный зверь. Тривиальный случай (приведение от указателя к производному классу к указателю на базовый класс), ну, в общем, тривиальный, без проверки во время выполнения, и это единственный случай, когда он отлично работает на неполиморфных классах (у которых нет RTTI) .

Второй самый простой случай - когда вы пытаетесь разыграть Base *, чей «реальный» тип - от Derived * до Derived *. Это просто вопрос сравнения указателя RTTI Derived * с указателем, связанным с объектом, который будет приведен; если они совпадают, приведение выполнено успешно.

Общий случай, напротив, гораздо сложнее. dynamic_cast должен проверить иерархию классов указанного объекта, следуя некоторым правилам, которые гарантируют, что никакие бессмысленные преобразования не выполняются (эти правила определены в стандарте C ++, §5.2.7 ¶8). Эта проверка во время выполнения выполняется с использованием информации о типе, описанной выше. Если приведение выполнено успешно, вы получите правильно приведенный указатель / ссылку, в противном случае вы получите исключение NULL (если это указатель) или bad_cast (если это указание).

А как насчет неполиморфных типов? Что ж, они более или менее исключены из RTTI: typeid будет возвращать информацию о статическом типе (которая обычно бесполезна), dynamic_cast будет разрешать только апкастинг (который может быть проверен во время компиляции), при компиляции даункастинг будет неудачным -время.

Смысл этого решения, вероятно, заключался не в создании "сложных" типов, которые должны быть простыми: расширение RTTI до каждого типа с использованием скрытого указателя сделало бы невозможным использование типов POD ( «Обычные старые данные», т.е. вещи, аналогичные традиционным C struct s).

Кроме того, в конце концов, если у вас нет virtual методов, вам, вероятно, не нужны dynamic_cast и type_id: если у вас есть иерархии объектов, которыми манипулирует указатель на базовые классы, вы должен иметь как минимум деструктор virtual, иначе могут произойти неприятные вещи (если вы delete a Derived ссылаетесь на него через Base *, а деструктор не является virtual, вы вызовете только деструктор Base, но не тот, который определен в Derived - определенно не очень).

Теперь на другие ваши комментарии можно легко ответить:

Если вы получите информацию о типе Ring * для OneRing, это будет Ring *.Почему они решили не размещать информацию на занятиях, я понятия не имею.Это делает typeid () практически бесполезным - он даже возвращает закрытый класс с единственным членом, к которому у вас есть доступ, именем char *!Это как 11/10 по шкале недружелюбия и бесполезности - они сделали это нарочно?

Вы получаете Ring *, только если Ring не является полиморфным.Вы должны сделать по крайней мере один член Ring полиморфным для работы RTTI (и, как указано, вероятно, по крайней мере, деструктор Ring должен быть виртуальным).Полезность типа, возвращаемого typeid, заключается в том, что его можно сравнить в равенстве с другим type_info - элемент name полезен только для целей отладки (фактически, если говорить о стандарте, он всегда может вернуть(пустая строка).

Вы можете безопасно свернуть дерево наследования с помощью динамического приведения, но опять же вы не сможете сделать это, если у вас нет общего предка наследования для всех полученных данных и вы не знаете его.

Это необходимо, потому что, если у вас есть универсальный void *, компилятор не может знать, где находится vptr - черт, он даже не знает, есть ли у этой вещи вптр, это может быть просто int *!:-)

Другими словами, компилятор должен иметь некоторую минимальную информацию о статическом типе, чтобы иметь возможность определить, есть ли какая-либо информация о типе, доступная во время выполнения, для выполнения решения (если класс не полиморфен или даже не являетсяclass приведение завершается неудачно во время компиляции ) и где находится эта информация (т.е. где vptr находится в объекте - информация, которая может быть выведена из статического типа любого предка).

Если вы выходите за рамки вышеописанного сценария или у вас нет RTTI по ​​какой-либо причине, вы попадаете в пресловутый ручей, потому что нет способа отловить исключение или сгенерировать ошибку в результате неправильного приведения.если вы сами не внедрили какую-либо форму RTTI.

Вот так работает язык;другие варианты реализации RTTI (например, отслеживание типа каждого указателя) были бы намного более сложными (если это возможно) и мало дополнительной полезности.Если вам нужны широкие возможности отражения, C ++ - не тот путь, вам нужно использовать управляемые языки.

Небольшой пример для подведения итогов:

#include <iostream>
#include <typeinfo>

using namespace std;

class Base_NoPolymorphic
{
public:
    void Name()
    {
        cout<<"Base_NoPolymorphic"<<endl;
    }
};

class Derived_NoPolymorphic : public Base_NoPolymorphic
{
public:
    void Name()
    {
        cout<<"Derived_NoPolymorphic"<<endl;
    }
};

class Base_Polymorphic
{
public:
    virtual void Name()
    {
        cout<<"Base_Polymorphic"<<endl;
    }
};

class Derived_Polymorphic : public Base_Polymorphic
{
public:
    virtual void Name()
    {
        cout<<"Derived_Polymorphic"<<endl;
    }
};

int main()
{
    Base_NoPolymorphic bnp;
    Derived_NoPolymorphic dnp;
    Base_Polymorphic bp;
    Derived_Polymorphic dp;

    // Non-polymorphic stuff: nothing works as expected
    Base_NoPolymorphic *bnpp=&bnp, *dnpp=&dnp;
    // These will always behave as Base_NoPolymorphic
    cout<<typeid(bnpp).name()<<" - ";
    bnpp->Name();
    cout<<typeid(dnpp).name()<<" - ";
    dnpp->Name();
    // This does not compile, because you're trying a cast down the class hierachy
    // on a non-polymorphic type
    // cout<<dynamic_cast<Derived_NoPolymorphic *>(dnpp)<<endl;

    //Polymorphic stuff: everything works fine
    Base_Polymorphic *bpp=&bp, *dpp=&dp;
    // These will behave correctly depending on their "real" type
    cout<<typeid(bpp).name()<<" - ";
    bpp->Name();
    cout<<typeid(dpp).name()<<" - ";
    dpp->Name();
    // This will succeed (output != 0)
    cout<<dynamic_cast<Derived_Polymorphic *>(dpp)<<endl;

    // Notice that also with polymorphic stuff a void * remains a black box
    void * ptr=&dp;
    cout<<typeid(ptr).name()<<endl;
    // This does not compile, because the source static type is a non-class type
    // cout<<dynamic_cast<Derived_Polymorphic *>(ptr)<<endl;
    return 0;
}

Вывод на моей машине (g ++ 4.5):

P18Base_NoPolymorphic - Base_NoPolymorphic
P18Base_NoPolymorphic - Base_NoPolymorphic
P16Base_Polymorphic - Base_Polymorphic
P16Base_Polymorphic - Derived_Polymorphic
0x7fffd7ad6890
Pv

, как и ожидалось: typeid только для неполиморфных типов возвращаетстатическая информация о типе (Pv - это «имя» RTTI g ++ для void *), в то время как отлично работает с полиморфными типами;то же самое верно для virtual / невиртуальных вызовов методов через указатель базового класса.dynamic_cast успешно работает с полиморфными типами, и, если вы раскомментируете другие dynamic_cast, вы увидите, что он не компилируется.

0 голосов
/ 22 июня 2011

Я понимаю, что C ++ не предоставляет RTTI в большинстве случаев (скажем так, я не могу изменить настройки компилятора и включить его, ради аргумента.).

Это просто неправильно.C ++ предоставляет typeid() и dynamic_cast, и если ваш компилятор не предоставляет его или вы его не включили, то это ваша проблема.

Я сделалнемного погуглил и обнаружил, что есть такие вещи, как typeid и type_of, но есть множество причин не использовать их.

type_of - это C ++ / CLI, не C ++, но typeid() отлично работает на С ++, и любой, кто сказал вам не использовать его, - идиот, и это в значительной степени так.Есть фанатики из 1980-х, которые скажут вам не использовать его для производительности, бинарного раздувания или какой-то другой хрени, которая могла бы быть полезной 30 лет назад, но в C ++, написанном в этом веке, нет никаких оправданий или причин не использоватьtypeid().

Вы также можете использовать dynamic_cast, и это именно то, для чего оно в языке.

...