// хочет передать его функции Fun
Derived* pd = (Derived*)pb; // Плохо: преобразование
Fun(pd); // в стиле С
}
Если функция Gun имеет доступ к определению Derived
(например, при включении заголовочного файла derived.h
), то компилятор имеет всю необходимую информацию о размещении объекта, чтобы выполнить все необходимые действия по корректировке указателя при преобразовании от Base
к Derived
. Но если автор Gun
забыл включить соответствующий файл определения, и функции Gun
видно только предварительное объявление класса Derived
, то компилятор будет полагать, что Base
и Derived
— несвязанные типы, и интерпретирует биты указателя Base*
как биты указателя Derived*
, не делая никаких коррекций, которые могут диктоваться размещением объекта в памяти!
Коротко говоря, если вы забудете включить определение класса, то ваш код может аварийно завершиться без видимых причин, при том что компилятор не сообщил ни об одной ошибке. Избавимся от проблемы следующим способом:
extern void Fun(Derived*);
void Gun(Base* pb) {
// Если мы гарантированно знаем, что pb на самом деле
// указывает на объект типа Derived:
// Преобразование в стиле С++
Derived* pd = static_cast<Derived*>(pb);
// В противном случае следует использовать
// = dynamic_cast<Derived*>(pb);
Fun(pd);
}
Теперь, если у компилятора недостаточно статической информации об отношениях между Base
и Derived
, он выведет сообщение об ошибке, вместо того чтобы автоматически применить побитовое (и потенциально опасное) преобразование reinterpret_cas
t (см. рекомендацию 92).
Преобразования в стиле С++ могут защитить корректность вашего кода в процессе эволюции системы. Пусть, например, у вас есть иерархия с корнем в 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), использование автоматизированного инструментария для их отслеживания — неплохая идея.
96. Не применяйте memcpy
или memcmp
к не-POD типам
Не работайте рентгеновским аппаратом (см. рекомендацию 91). Не используйте memcpy
и memcmp
для копирования или сравнения чего-либо структурированного более, чем обычная память.
Функции memcpy
и memcmp
нарушают систему типов. Использовать memcpy
для копирования объектов — это то же, что использовать ксерокс для копирования денег, а сравнивать объекты при помощи memcmp
— то же, что сравнивать двух леопардов по количеству пятен. Инструменты и методы могут казаться подходящими для выполнения работы, но они слишком грубы для того, чтобы сделать ее правильно.
Объекты С++ предназначены для сокрытия данных (возможно, наиболее важный принцип в разработке программного обеспечения; см. рекомендацию 11). Объекты скрывают данные (см. рекомендацию 41) и предоставляют точные абстракции для копирования этих данных посредством конструкторов и операторов присваивания (см. рекомендации с 52 по 55). Пройтись по ним грубым инструментом типа memcpy
— серьезное нарушение принципа сокрытия информации, которое зачастую приводит к утечкам памяти и ресурсов (в лучшем случае), аварийному завершению программы (в случае похуже) или неопределенному поведению (в самом худшем случае). Например:
{
// Создаем два int в памяти
shared_ptr<int> p1(new int), p2(new int);