Преобразования в стиле С++ могут защитить корректность вашего кода в процессе эволюции системы. Пусть, например, у вас есть иерархия с корнем в
Employee
, и вам надо определить уникальный идентификатор
ID
для каждого объекта
Employee
. Вы можете определить ID как указатель на сам объект
Employee
. Указатели однозначно идентифицируют объекты, на которые указывают, и могут сравниваться на равенство друг другу — что в точности то, что нам и надо. Итак, запишем:
typedef Employee* EmployeeID;
Employee& Fetch(EmployeeID id) {
return *id;
}
Пусть
вы кодируете часть системы с данным дизайном. Пусть позже вам требуется сохранять ваши записи в реляционной базе данных. Понятно, что сохранение указателей — не то, что вам требуется. В результате вы изменяете дизайн так, чтобы каждый объект имел уникальный целочисленный идентификатор. Тогда целочисленный идентификатор может храниться в базе данных, а хэш-таблица отображает идентификаторы на объекты
Employee
. Теперь
typedef
выглядит следующим образом:
typedef int EmployeeID;
Employee& Fetch( EmployeeID id ) {
return employeeTable_.lookup(id);
}
Это корректный дизайн, и вы ожидаете, что любое неверное употребление
EmployeeID
должно привести к ошибке времени компиляции. Так и получается, за исключением следующего небольшого фрагмента:
void TooCoolToUseNewCasts(EmployeeID id) {
Secretary* pSecretary = (Secretary*)id; // Плохо:
// ... // преобразование в стиле С
}
При использовании старой инструкции
typedef
преобразование в стиле С выполняет
static_cast
, при новой будет выполнено
reinterpret_cast
с некоторым целым числом, что даст нам неопределенное поведение программы (см. рекомендацию 92).
Преобразования в стиле С++ проще искать в исходных текстах при помощи автоматического инструментария наподобие
grep
(но никакое регулярное выражение
grep
не позволит выловить синтаксис преобразования типов в стиле С). Поскольку преобразования очень опасны (в особенности
static_cast
для указателей и
reinterpret_cast
; см. рекомендацию 92), использование автоматизированного инструментария для их отслеживания — неплохая идея.
Не работайте рентгеновским аппаратом (см. рекомендацию 91). Не используйте
memcpy
и
memcmp
для копирования или сравнения чего-либо структурированного более, чем обычная память.
Обсуждение
Функции
memcpy
и
memcmp
нарушают систему типов. Использовать
memcpy
для копирования объектов — это то же, что использовать ксерокс для копирования денег, а сравнивать объекты при помощи
memcmp
— то же, что сравнивать двух леопардов по количеству пятен. Инструменты и методы могут казаться подходящими для выполнения работы, но они слишком грубы для того, чтобы сделать ее правильно.
Объекты С++ предназначены для сокрытия данных (возможно, наиболее важный принцип в разработке программного обеспечения; см. рекомендацию 11). Объекты скрывают данные (см. рекомендацию 41) и предоставляют точные абстракции для копирования этих данных посредством конструкторов и операторов присваивания (см. рекомендации с 52 по 55). Пройтись по ним грубым инструментом типа
memcpy
— серьезное нарушение принципа сокрытия информации, которое
зачастую приводит к утечкам памяти и ресурсов (в лучшем случае), аварийному завершению программы (в случае похуже) или неопределенному поведению (в самом худшем случае). Например:
{
// Создаем два int в памяти
shared_ptr<int> p1(new int), p2(new int);
memcpy(&p1, &p2, sizeof(p1)); // Так делать нельзя!!!
} // Утечка памяти: p2 никогда не удаляется
// повреждение памяти: p1 удаляется дважды
Неверное применение
memcpy
может влиять на такие фундаментальные свойства, как тип и сущность объекта. Компиляторы часто добавляют к полиморфным объектам скрытые данные (так называемый указатель на виртуальную таблицу), которые определяют сущность объекта во время выполнения программы. В случае множественного наследования в объекте содержится несколько таких таблиц, с различными смещениями внутри объекта, и большинство реализаций добавляют дополнительные внутренние указатели при виртуальном наследовании. При обычном использовании компилятор принимает меры для корректного управления всеми скрытыми полями; применение
memcpy
способно внести в этот механизм только хаос.
Аналогично, функция
memcmp
— неподходящий инструмент для сравнения чего-то более сложного, чем просто наборы битов. Иногда эта функция делает слишком мало (например, сравнение строк в стиле С — не то же, что и сравнение указателей, при помощи которых эти строки реализованы). А иногда, как это ни парадоксально,
memcmp
делает слишком много (например,
memcmp
может совершенно напрасно сравнивать байты, которые не являются частью состояния объекта, такие как заполнители, вставленные компилятором для выравнивания). В обоих случаях результат сравнения оказывается неверным.
Ссылки
[Dewhurst03] §50 • [Stroustrup94] §11.4.4
97. Не используйте объединения для преобразований
Резюме
Хитрость все равно остается ложью: объединения можно использовать для получения "преобразования типа без преобразования", записывая информацию в один член и считывая из другого. Однако это еще более опасно и менее предсказуемо, чем применение
reinterpret_cast
(см. рекомендацию 92).
Обсуждение
Не считывайте данные из поля объединения, если последняя запись была не в это же поле. Чтение из поля, отличного от поля, в которое производилась запись, имеет неопределенное поведение, и использование этого метода еще хуже, чем применение
reinterpret_cast
(см. рекомендацию 92); в последнем случае компилятор, как минимум, может предупредить программиста и не допустить "невозможной интерпретации" наподобие указателя в
char
. При использовании для этой цели объединения никакая интерпретация не приведет к ошибке времени компиляции (как и к надежному результату).
Рассмотрим фрагмент кода, предназначенного для сохранения значения одного типа (char*) и выборки битов этого значения в виде величины иного типа (
long
):
union {
long intValue_;
char* pointerValue_;
};
pointerValue_ = somePointer;
long int gotcha = intValue_;
Здесь есть две проблемы.
• Данный код требует слишком многого. Он полагается на то, что
sizeof(long)
и
sizeof(char*)
равны и что их битовые представления идентичны. Эти утверждения справедливы не для всех возможных реализаций (см. рекомендацию 91).