Не пытайтесь делать какие-то предположения о том, как именно объекты представлены в памяти. Как именно следует записывать и считывать объекты из памяти — пусть решают типы объектов.
Стандарт С++ дает очень мало гарантий по поводу представления типов в памяти.
• Целые числа используют двоичное представление.
• Для отрицательных чисел используется дополнительный код числа в двоичной системе.
• Обычные старые типы (Plain Old Data, POD[5]) имеют совместимое с С размещение в памяти: переменные-члены хранятся в порядке их объявления.
• Тип int
занимает как минимум 16 битов.
В частности, достаточно распространенные соглашения
• Размер int
не равен ни 32 битам, ни какому-либо иному фиксированному размеру.
• Указатели и целые числа не всегда имеют один и тот же размер и не могут свободно преобразовываться друг в друга.
• Размещение класса в памяти не всегда приводит к размещению базового класса и членов в указанном порядке.
• Между членами класса (даже если они являются POD) могут быть промежутки в целях выравнивания.
• offsetof
работает только для POD, но не для всех классов (хотя компилятор может и не сообщать об ошибках).
• Класс может иметь скрытые поля.
• Указатели могут быть совсем не похожи на целые числа. Если два указателя упорядочены и вы можете преобразовать их в целые числа, то получающиеся значения могут быть упорядочены иначе.
• Нельзя переносимо полагаться на конкретное размещение автоматических переменных в памяти или на направление роста стека.
• Указатели на функции могут иметь размер, отличный от размера указателя void*
, несмотря на то, что некоторые API заставляют вас предположить, что их размеры одинаковы.
• Из-за вопросов выравнивания вы не можете записывать ни один объект по произвольному адресу в памяти.
Просто корректно определите типы, а затем читайте и записывайте данные с использованием указанных типов вместо работы с отдельными битами, словами и адресами. Модель памяти С++ гарантирует эффективную работу, не заставляя вас при этом работать с представлениями данных в памяти. Так и не делайте этого.
92. Избегайте reinterpret_cast
Как гласит римская пословица, у лжи короткие ноги. Не пытайтесь использовать reinterpret_cast
, чтобы заставить компилятор рассматривать биты объекта одного типа как биты объекта другого типа. Такое действие противоречит безопасности типов.
Вспомните:
Преобразование reinterpret_cast
отражает представления программиста о представлении объектов в памяти, т.е. программист берет на себя ответственность за то, что он лучше компилятора знает, что можно и что нельзя. Компилятор молча сделает то, что вы ему скажете, но применять такую грубую силу в отношениях с компилятором — последнее дело. Избегайте каких-либо предположений о представлении данных, поскольку такие предположения очень сильно влияют на безопасность и надежность вашего кода.
Кроме того, реальность такова, что результат применения reinterpret_cast
еще хуже, чем просто насильственная интерпретация битов объекта (что само по себе достаточно нехорошо). За исключением некоторых гарантированно обратимых преобразований результат работы reinterpret_cast
зависит от реализации, так что вы даже не знаете точно, как именно он будет работать. Это очень ненадежное и непереносимое преобразование.
Некоторые низкоуровневые специфичные для данной системы программы могут заставить вас применить reinterpret_cast
к потоку битов, проходящих через некоторый порт, или для преобразования целых чисел в адреса. Используйте такое небезопасное преобразование как можно реже и только в тщательно скрытых за абстракциями функциях, чтобы ваш код можно было переносить с минимальными изменениями. Если вам требуется преобразование между указателями несвязанных типов, лучше выполнять его через приведение к void*
вместо непосредственного использования reinterpret_cast
, т.е. вместо кода
T1* p1 = ... ;
T2* p2 = reinterpret_cast<T2*>(p1);
лучше писать
T1* p1 = ...;
void* pV = p1;
T2* p2 = static_cast<T2*>(pV);
93. Избегайте применения static_cast
к указателям
К указателям на динамические объекты не следует применять преобразование static_cast
. Используйте безопасные альтернативы — от dynamic_cast
до перепроектирования.
Подумайте о замене static_cast
более мощным оператором dynamic_cast
, и вам не придется запоминать, в каких случаях применение static_cast
безопасно, а в каких — чревато неприятностями. Хотя dynamic_cast может оказаться немного менее эффективным преобразованием, его применение позволяет обнаружить неверные преобразования типов (но не забывайте о рекомендации 8). Использование static_cast
вместо dynamic_cast
напоминает экономию на ночном освещении, когда выигрыш доллара в год оборачивается переломанными ногами.
При проектировании постарайтесь избегать понижающего приведения. Перепроектируйте ваш код таким образом, чтобы такое приведение стало излишним. Если вы видите, что передаете в функцию базовый класс там, где в действительности потребуется производный класс, проследите всю цепочку вызовов, чтобы понять, где же оказалась потерянной информация о типе; зачастую изменение пары прототипов оказывается замечательным решением, которое к тому же делает код более простым и