193просмотров
11 февраля 2024 г.
Score: 212
Грабли со static_cast Недавно наткнулся на забавную багу в коде, которая заставила задуматься над поведением, казалось бы банального, static_cast.
Давайте рассмотрим следующее дерево наследования: struct Base {};
struct ActualChild : Base {};
struct OtherChild : Base {}; Давайте теперь сделаем static_cast между братскими классами: ActualChild obj;
ActualChild actual_ptr = &obj;
Base base_ptr = actual_ptr; OtherChild other_ptr = static_cast<OtherChild>(base_ptr); В теории static_cast не должен делать ничего специфичного. Он просто интерпретирует память как объект данного типа. Пока что вроде особых проблем нет, но давайте добавим в класс OtherChild метод и попробуем его вызвать: struct OtherChild : Base { int foo() { return 0; }
};
—
other_ptr->foo(); Полный код доступен тут: https://godbolt.org/z/Yvr5oMd7h Странным образом результатом такого вызова будет вполне корректный возврат значения 0. Впрочем если у вас опыта с плюсами побольше моего – для вас ничего странного в этом нет. Компилятор просто вставил вызов метода OtherChild::foo() и передал ему соответствующий указатель в качестве свойства this. Это можно и увидеть в окне с ассемблером в следующих строках: mov rdi, qword ptr [rbp - 32] call OtherChild::foo()
Тут логично возникает два вопроса:
1. Что будет если метод foo станет виртуальным?
2. Что будет если метод foo решит использовать свойства класса OtherClass. Ответ на первый вопрос я для краткости заметки отложу на следующий раз, а сейчас давайте попробуем использовать какое-нибудь свойство: struct OtherChild : Base { int foo() { return value; } int value;
}; …
other_ptr->foo(); Полный код: https://godbolt.org/z/raccns5hs Результатом подобного вызова будет считывание случайного куска памяти. Чтобы понять как это работает давайте посмотрим на ассемблер: mov rax, qword ptr [rbp - 8]
mov eax, dword ptr [rax] В данном случае в rbp лежит указатель на объект класса ActualChild (this). Компилятор же в данном участке думает что он работает с OtherChild. Для этого класса он знает, что значение value лежит в первых 4 байтах.
По сути передав в этот метод объект класса ActualChild мы просто считаем память лежащую в первых 4 байт от этого указателя. Давайте собственно проверим так ли это следующим кодом: struct ValueHolder { int value = 42;
}; int main() { ValueHolder holder; OtherChild other_ptr = static_cast<OtherChild>( (void*)&holder); return other_ptr->foo();
} https://godbolt.org/z/b561Yajqa И действительно результатом данной программы будет 42. В следующий раз я попробую поиграться с виртуальными методами и посмотреть как компилятор будет вести себя в этом случае. А в качестве вывода статьи скажу, что если вы не уверены что лежит под указателем на родительский класс – пользуйтесь dynamic_cast, хоть это и дороже и требует RTTI.