id_
, который хранит тип каждой фигуры — прямоугольник, окружность и т.д. Рисующий код выполняет необходимые действия в зависимости от выбранного типа:
class Shape { // ...
enum { RECTANGLE, TRIANGLE, CIRCLE } id_;
void Draw() const {
switch (id_) { // плохой метод
case RECTANGLE:
// ... Код для прямоугольника …
break;
case TRIANGLE:
// ... Код для треугольника …
break;
case CIRCLE:
// ... Код для окружности …
break;
default: // Плохое решение
assert(!"при добавлении нового типа надо "
"обновить эту конструкцию" );
break;
}
}
};
Такой код сгибается под собственным весом, он хрупок, ненадежен и сложен. В частности, он страдает транзитивной циклической зависимостью, о которой говорилось в рекомендации 22. Ветвь по умолчанию конструкции switch
— четкий симптом синдрома "не знаю, что мне делать с этим типом". И все эти болезненные неприятности полностью исчезают, стоит только вспомнить, что С++ — объектно-ориентированный язык программирования:
class Shape { // ...
virtual void Draw() const = 0; // Каждый производный
// класс реализует свою функцию
};
В качестве альтернативы (или в качестве дополнения) рассмотрим реализацию, которая следует совету по возможности принимать решения во время компиляции (см. рекомендацию 64):
template
void Draw(const S& shape) {
shape.Draw(); // может быть виртуальной, а может и не быть
}; // См. рекомендацию 64
Теперь ответственность за рисование каждой геометрической фигуры переходит к реализации самой фигуры, и синдром "не знаю, что делать с этим типом" просто невозможен.
91. Работайте с типами, а не с представлениями
Не пытайтесь делать какие-то предположения о том, как именно объекты представлены в памяти. Как именно следует записывать и считывать объекты из памяти — пусть решают типы объектов.
Стандарт С++ дает очень мало гарантий по поводу представления типов в памяти.
• Целые числа используют двоичное представление.
• Для отрицательных чисел используется дополнительный код числа в двоичной системе.
• Обычные старые типы (Plain Old Data, POD[5]) имеют совместимое с С размещение в памяти: переменные-члены хранятся в порядке их объявления.
• Тип int
занимает как минимум 16 битов.
В частности, достаточно распространенные соглашения
• Размер int
не равен ни 32 битам, ни какому-либо иному фиксированному размеру.
• Указатели и целые числа не всегда имеют один и тот же размер и не могут свободно преобразовываться друг в друга.
• Размещение класса в памяти не всегда приводит к размещению базового класса и членов в указанном порядке.
• Между членами класса (даже если они являются POD) могут быть промежутки в целях выравнивания.
• offsetof
работает только для POD, но не для всех классов (хотя компилятор может и не сообщать об ошибках).
• Класс может иметь скрытые поля.
• Указатели могут быть совсем не похожи на целые числа. Если два указателя упорядочены и вы можете преобразовать их в целые числа, то получающиеся значения могут быть упорядочены иначе.
• Нельзя переносимо полагаться на конкретное размещение автоматических переменных в памяти или на направление роста стека.
• Указатели на функции могут иметь размер, отличный от размера указателя void*
, несмотря на то, что некоторые API заставляют вас предположить, что их размеры одинаковы.
• Из-за вопросов выравнивания вы не можете записывать ни один объект по произвольному адресу в памяти.
Просто корректно определите типы, а затем читайте и записывайте данные с использованием указанных типов вместо работы с отдельными битами, словами и адресами. Модель памяти С++ гарантирует эффективную работу, не заставляя вас при этом работать с представлениями данных в памяти. Так и не делайте этого.
92. Избегайте reinterpret_cast