Обучающие курсы:

Обучение профессии "Разработчик C#" + стажировка в Mail.ru
Обучение профессии "Разработчик Python" + трудоустройство
Обучение профессии "Веб-разработчик" + стажировка в Mail.ru


Главная страница
Библиотека (скачать книги)
Скачать софт
Введение в программирование
Стандарты для C++
Уроки по C#
Уроки по Python
HTML
Веб-дизайн
Ассемблер в среде Windows
ActiveX
Javascript
Общее о Линукс
Линукс - подробно
Линукс - новое
Delphi
Паскаль для начинающих
Турбопаскаль
Новости
Партнеры
Наши предложения
Архив новостей







91. Работайте с типами, а не с представлениями

Резюме

Не пытайтесь делать какие-то предположения о том, как именно объекты представлены в памяти. Как именно следует записывать и считывать объекты из памяти — пусть решают типы объектов.

Обсуждение

Стандарт C++ дает очень мало гарантий по поводу представления типов в памяти.

  • Целые числа используют двоичное представление.

  • Для отрицательных чисел используется дополнительный код числа в двоичной системе.

  • Обычные старые типы (Plain Old Data, POD5) имеют совместимое с С размещение в памяти: переменные-члены хранятся в порядке их объявления.

  • Тип int занимает как минимум 16 битов.

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

  • Размер int не равен ни 32 битам, ни какому-либо иному фиксированному размеру.

  • Указатели и целые числа не всегда имеют один и тот же размер и не могут свободно преобразовываться друг в друга.

  • Размещение класса в памяти не всегда приводит к размещению базового класса и членов в указанном порядке.

  • Между членами класса (даже если они являются POD) могут быть промежутки в целях выравнивания.

  • offsetof работает только для POD, но не для всех классов (хотя компилятор может и не сообщать об ошибках).

  • Класс может иметь скрытые поля.

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

  • Нельзя переносимо полагаться на конкретное размещение автоматических переменных в памяти или на направление роста стека.


5Неформально POD означает любой тип, представляющий собой набор простых данных, возможно, с пользовательскими функциями-членами для удобства. Говоря более строго, POD представляет собой класс или объединение, у которого нет пользовательского конструктора, копирующего присваивания, и деструктора, а также нет (нестатических) членов-данных, являющихся ссылками, указателями на члены или не являющихся POD. — Прим. ред.

  • Указатели на функции могут иметь размер, отличный от размера указателя void*, несмотря на то, что некоторые API заставляют вас предположить, что их размеры одинаковы.

  • Из-за вопросов выравнивания вы не можете записывать ни один объект по произвольному адресу в памяти.

Просто корректно определите типы, а затем читайте и записывайте данные с использованием указанных типов вместо работы с отдельными битами, словами и адресами. Модель памяти C++ гарантирует эффективную работу, не заставляя вас при этом работать с представлениями данных в памяти. Так и не делайте этого.

92. Избегайте reinterpret_cast

Резюме

Как гласит римская пословица, у лжи короткие ноги. Не пытайтесь использовать reinterpret_cast, чтобы заставить компилятор рассматривать биты объекта одного типа как биты объекта другого типа. Такое действие противоречит безопасности типов.

Обсуждение

Вспомните: Если вы лжете компилятору, он будет мстить (Генри Спенсер).

Преобразование reinterpret_cast отражает представления программиста о представлении объектов в памяти, т.е. программист берет на себя ответственность за то, что он лучше компилятора знает, что можно и что нельзя. Компилятор молча сделает то, что вы ему скажете, но применять такую грубую силу в отношениях с компилятором — последнее дело. Избегайте каких-либо предположений о представлении данных, поскольку такие предположения очень сильно влияют на безопасность и надежность вашего кода.

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

Исключения

Некоторые низкоуровневые специфичные для данной системы программы могут заставить вас применить reinterpret_cast к потоку битов, проходящих через некоторый порт, или для преобразования целых чисел в адреса. Используйте такое небезопасное преобразование как можно реже и только в тщательно скрытых за абстракциями функциях, чтобы ваш код можно было переносить с минимальными изменениями. Если вам требуется преобразование между указателями несвязанных типов, лучше выполнять его через приведение к void* вместо непосредственного использования reinterpret_cast, т.е. вместо кода

T1* p1 = ... ;
T2* р2 = reinterpret_cast<T2*>( p1 );

лучше писать

T1* p1 = ... ;
void* pV = p1;
T2* р2 = static_cast<T2*>( pv );

93. Избегайте применения static_cast к указателям

Резюме

К указателям на динамические объекты не следует применят преобразование static_cast. Используйте безопасные альтернативы — от dynamic_cast до перепроектирования.

Обсуждение

Подумайте о замене static_cast более мощным оператором dynamic_cast, и вам не придется запоминать, в каких случаях применение static_cast безопасно, а в каких — чревато неприятностями. Хотя dynamic_cast может оказаться немного менее эффективным преобразованием, его применение позволяет обнаружить неверные преобразования типов (но не забывайте о рекомендации 8). Использование static_cast вместо dynamic_cast напоминает экономию на ночном освещении, когда выигрыш доллара в год оборачивается переломанными ногами.

При проектировании постарайтесь избегать понижающего приведения. Перепроектируйте ваш код таким образом, чтобы такое приведение стало излишним. Если вы видите, что передаете в функцию базовый класс там, где в действительности потребуется производный класс, проследите всю цепочку вызовов, чтобы понять, где же оказалась потерянной информация о типе; зачастую изменение пары прототипов оказывается замечательным решением, которое к тому же делает код более простым и понятным.

Чрезмерное применение понижающего приведения может служить признаком слишком бедного интерфейса базового класса. Такой интерфейс может привести к тому, что большая функциональность определяется в производных классах, и всякий раз при необходимости расширения интерфейса приходится использовать понижающее приведение. Одно из решений данной проблемы — перепроектирование базового интерфейса в целях повышения функциональности.

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

template<class To, class From> To checked_cast(From* from) {
    assert(dynamic_cast<To>(from) ==
           static_cast<To>(from) && "checked_cast failed" );
    return static_cast<To>( from );
}

template<class To, class From> To checked_cast(From& from) {
    typedef tr1::remove_reference<To>::type* ToPtr; //

    assert(dynamic_cast<ToPtr>(&from) ==
           static_cast<ToPtr>(&from) && "checked_cast failed" );
           return static_cast<To>( from );
}

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



 
 

Библиотека программиста. 2009.
Администратор: admin@programmer-lib.ru