#cpp #типы #преобразование_типов
Привет всем. Нужно разобраться как работает static_cast, dynamic_cast, да и вообще любой cast из серии кастов, но особенно эти 2, чтобы потом далее без помощи думать над casts. На сколько смог вычитать, static_cast занимается тем, что берет и побитово( или все же побайтово?) копирует данные одного типа в данные другого типа. Вроде просто, но мне еще интересна реализация. Как найти релизацию? В visual studio я не могу перейти по goToDeclaration/defenition к описанию тела этой функции. *_cast вообще функция или что это? Похоже на какую-то типа шаблонную функцию или что-то типа generic в C#. dynamic_cast. Преобразование с проверкой. То есть на сколько понял он позволяет делать upCast/downCas, но не позволяет делать crossCast. Тут у меня холиварный большой вопрос: зачем делать upCast - понятно, на этом работает полиморфизм. А вот downCast зачем в С++ нужен? Я пытался на stackowerflow найти ответ, читая посты с подобным вопросом на c#, но я пока не могу думать глобальными концепциями c#, потому мало что понял. Можно еще приводить один тип объектов к другому (crossCast). Ну это для меня вообще дремучий лесище. Во-первых: указатели-то может как-то и приведутся, но а вообще как это будет работать-то??? Для этого нужно знать как вообще работают недра вызова методов (про раннее и позднее связывание знаю). Во-вторых: а зачем такое может быть нужно??? Можно пример попроще? В-третьих: я так понимаю, что после приведения одного объекта к другому, поля перемешаются и будут содержать мусор. Так как информация о том как приводить не имеется, на сколько я знаю, в классе нет такой информации. Ну а методы вообще непонятно как будут работать. Помогите пожалуйста залатать столь огромную дыру в моих знаниях)
Ответы
Ответ 1
На сколько смог вычитать, static_cast занимается тем, что берет и побитово (или все же побайтово?) копирует данные одного типа в данные другого типа. Ни одно из преобразований в С++ не работает "побитово" или "побайтово". Потому они и называются преобразованиями. Преобразования выполняют конвертирование данных из одного формата в другой (зачастую - нетривиальное). В общем случае с "побитовой" точки зрения представление результата такого преобразования не имеет ничего общего с представлением исходного значения. Побитовым/побайтовым копированием занимается функция memcpy, а не преобразования. static_cast - пожалуй самый универсальный каст. Он умеет делать арифметические преобразования, преобразования указателей в/из типа void *, преобразования объектных указателей вверх-вниз по иерархии, преобразования указателей на члены вверх-вниз по иерархии и т.п. Как найти релизацию? Реализацию чего? Все преобразования являются операторами языка и, соответственно, реализуются на уровне ядра языка. dynamic_cast. Преобразование с проверкой. То есть на сколько понял он позволяет делать upCast/downCas, но не позволяет делать crossCast. Тут у вас наблюдается какая-то терминологическая путаница. Наоборот, dynamic_cast - это единственный из кастов, который умеет делать cross-cast. Cross-cast-ом называется прямое преобразование от одного базового подобъекта к другому базовому подобъекту в рамках одного общего содержащего объекта. Т.е. сross-cast может возникать только в условиях множественного наследования #includestruct B { virtual ~B() {} }; struct C {}; struct D : B, C {}; int main() { D d; B *b = &d; // <- upcast C *c1 = &d; // <- upcast C *c2 = dynamic_cast (b); // <- cross-cast assert(c1 == c2); } Также dynamic_cast умеет делать downcast сквозь виртуальное наследование (чего не умеет делать static_cast) #include struct A { virtual ~A() {} }; struct B : virtual A {}; struct C : virtual A {}; struct D : B, C {}; int main() { D d; A *a = &d; // <- upcast D *d1 = dynamic_cast (a); // <- downcast, OK assert(d1 == &d); D *d2 = static_cast (a); // <- downcast, ERROR } dynamic_cast предназначен для нетривиальных полиморфных преобразований, но только внутри связной иерархии полиморфных классов. Ну и, разумеется, dynamic_cast умеет проверять корректность downcast-ов и сross-cast-ов. А вот downCast зачем в С++ нужен? Во многих жизненных ситуациях избежать downcast-а не удается, хоть с пьюристско-педантичной токи зрения downcast-ы возможно и идут вразрез с принципами чистого ООП. Во многих случаях код с downcast-ом получается проще, чем та же функциональность, реализованная с упорным избежанием downcast-а. Одним из классических примеров идиоматического применения downcast является Curiously Recurring Template Pattern ("странно повторяющийся шаблон"). Можно еще приводить один тип объектов к другому (crossCast). Приведение типов между посторонними несвязанными типами не является и не называется cross-cast-ом. Делать приведение типов между несвязанными между собой типами может только reinterpret_cast, но по поводу результата такого приведения и его полезности почти никаких гарантий языком не дается. В-третьих: я так понимаю, что после приведения одного объекта к другому, поля перемешаются и будут содержать мусор. Так приводить можно не сами объекты, а только указатели или ссылки на них. Такое преобразования делает именно и только reinterpret_cast. И да, при доступе через такие приведенные указатели или ссылки получится ерунда. Но лазить в данные через такие приведенные указатели язык все равно запрещает - поведение не определено (за редкими исключениями). Так что это функциональность из серии "сам виноват". Ответ 2
Все эти преобразования - это специальные функции (некоторые их называют "магическими"). Это функции на уровне компилятора. Поэтому, если очень интересно, что там внутри - нужно брать исходники компилятора и изучать. Логично предположить, что в случае студии вряд ли кто то их даст:) Но что же делать? а нужно смотреть в сгенерированный код. В студии есть возможность его посмотреть, но если не ограничивать себя только студией, то можно воспользоваться чудесным сайтом - https://gcc.godbolt.org/ Я немного экспериментировал и обычно весь код сводился просто к копированию указателя с одной переменной в другую или компилятор понимал, что я пытаюсь преобразовать два несовместимых типа и отказывался компилировать. То есть на сколько понял он позволяет делать upCast/downCas, но не позволяет делать crossCast. c upCast Вы разобрались сами. А вот с downCast сложнее. Обычно, его применение - признак плохого дизайна. Но... иногда сроки горят, заказчик хочет уже на вчера и приходиться. Эта ситуация может возникнуть, если в каком то общей функции (которая получае указатель на объект-предок) хочется вызвать специфическую функцию наследника. На сколько смог вычитать, static_cast занимается тем, что берет и побитово( или все же побайтово?) в данном случае все равно. Но я везде слышу "копия побитовая" или "побитово одинаковые". Видимо потому, что бит - как самая минимальная единица измерения информации известна давно. И все привыкли. А байт я думаю появился позже и размер был его не постоянный (хотя в стандарте с++ сказано, что байт - это строго 8 бит). Можно еще приводить один тип объектов к другому (crossCast). Ну это для меня вообще дремучий лесище. Во-первых: указатели-то может как-то и приведутся, но а вообще как это будет работать-то??? А кто его знает. Нужно смотреть на внутреннее устройство обеих объектов и тот код, который згенерировал компилятор. С переменными класса все понятно - в большинстве случаев они адресуются по смещению. С невиртуальными функциями также все просто. А вот с виртуальными... для этого вообще то придумали слово UB - неопределенное поведение. Что произойдет в этом случае - да все что угодно. Может работать, а может и комп взорвать. Во-вторых: а зачем такое может быть нужно??? Можно пример попроще? у программистов бывают страшные фантазии.Ответ 3
static_cast, по сути, вызывает соответствующий оператор преобразования или конструктор. Для встроеных типов всё может быть немного сложнее, но принцип тот же. Если написать int i = 1; double d = i; то будет неявно вызван оператор преобразования из int в double, то же самое можно сделать явно с помощью double d = static_cast(i). static_cast сработает только в том случае, если соответствующая функция преобразования уже существует (в виде конструктора или специального оператора преобразования), и делает ровно то, что делают эти функции преобразования. Естественно, в простых случаях всё максимально оптимизируется, но простое побитовое копирование встречается редко. Какие преобразования допустимы для встроенных типов можно посмотреть тут. Всё, что делает dynamic_cast - это приводит указатель/ссылку на базовый класс к указателю/ссылке на потомка, или бросает исключение, если объект не является этим потомком. Если объект вообще не является потомком этого базового класса т.е. такое преобразование не может произойти независимо от значения времени выполнения, произойдет ошибка компиляции. Обратное преобразование выполняется неявно, или, при желании, при помощи static_cast. reinterpret_cast интерпретирует указатель на объект одного типа как указатель на объект другого типа, независимо от того, насколько это правильно. Соответственно, в общем случае эта интерпретация некорректна и приводит к неопределенному поведению (то есть к проблемам). Для встроенных типов reinterpret_cast можно воспринимать как побитовое копирование. В отличие от static_cast, которое выполнит корректное преобразование, reinterpret_cast не делает этого, т.е. если double наложить на uint_64, получится неизвестное значение (зависит от аппаратного представления double и uint_64). Но можно, например, наложить значение uint_64 на указатель или наоборот, но даже в этом случае это будет работать только на 64-разрядных системах. UPD: Для таких вещей часто полезно смотреть, во что компилятор в итоге транслирует код, для чего удобно использовать https://gcc.godbolt.org/ (только нужно не забывать про оптимизатор и соответствующие флаги компиляции). Edit: dynamic_cast работает во время выполнения, определяя реальный тип объекта по его таблице виртуальных функций или подобным образом. Для этого тип, к которому приводится указатель должен быть либо унаследован от типа исходного указателя, или совпадать с ним. В других случаях такое преобразование невозможно в принципе, а потому ошибка возникает ещё на этапе компиляции. Например, нельзя привести int* к double* или std::runtume_error* к istream*, независимо тот того, на объекты каких типов они на самом указывают.
Комментариев нет:
Отправить комментарий