Но вот чего стоит избегать всегда – это каскадов из операторов dynamic_cast, то есть чего-то вроде такого кода:
class Window {...};
... // здесь определены производные классы
typedef std::vector> VPW;
VPW winPtrs;
...
for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
if (SpecialWindow1 *psw1=
dynamic_cast(iter->get())) {...}
else if (SpecialWindow2 *psw2=
dynamic_cast(iter->get())) {...}
else if (SpecialWindow2 *psw2=
dynamic_cast(iter->get())) {...}
...
}
В этом случае генерируется объемный и медленный код, к тому же он нестабилен, потому что при каждом изменении иерархии классов Window весь этот код нужно пересмотреть на предмет обновления. Например, если добавится новый производный класс, то вероятно, придется добавить еще одну ветвь в предложение if. Подобный код почти всегда должен быть заменен чем-то на основе вызова виртуальных функций.
В хорошей программе на C++ приведения типов используются очень редко, но полностью отказываться от них тоже не стоит. Так, показанное выше приведение int к double является разумным, хотя и не абсолютно необходимым (код может быть переписан с объявлением новой переменной типа double, инициируемой значением x). Как и большинство сомнительных конструкций, приведения типов должны быть изолированы насколько возможно. Обычно они помещаются внутрь функций, чей интерфейс скрывает от пользователей те некрасивые дела, что творятся внутри.
Что следует помнить
• Избегайте насколько возможно приведений типов, особенно dynamic_cast, в критичном по производительности коде. Если дизайн требует приведения, попытайтесь разработать альтернативу, где такой необходимости не возникает.
• Когда приведение типа необходимо, постарайтесь скрыть его внутри функции. Тогда пользователи смогут вызывать эту функцию вместо помещения приведения в их собственный код.
• Предпочитайте приведения в стиле C++ старому стилю. Их легче увидеть, и они более избирательны.
Правило 28: Избегайте возвращения «дескрипторов» внутренних данных
Представим, что вы работаете над приложением, имеющим дело с прямоугольниками. Каждый прямоугольник может быть представлен своим левым верхним углом и правым нижним. Чтобы объект Rectangle оставался компактным, вы можете решить, что описание определяющих его точек следует вынести из Rectangle во вспомогательную структуру:
class Point { // класс, представляющий точки
public:
Point(int x, int y);
...
void setX(int newVal);
void setY(int newVal);
...
};
struct RectData { // точки, определяющие Rectangle
Point ulhc; // ulhc – верхний левый угол
Point lrhc; // lrhc – нижний правый угол
};
class Rectangle {
...
private:
std::tr1::shared_ptr pData; // см. в правиле 13
}; // информацию о tr1::shared_ptr
Поскольку пользователям класса Rectangle понадобится определять его координаты, то класс предоставляет функции upperLeft и lowerRight. Однако Point – это определенный пользователем тип, поэтому, помня о том, что передача таких типов по ссылке обычно более эффективна, чем передача по значению (см. правило 20), эти функции возвращают ссылки на внутренние объекты Point:
class Rectangle {
public:
...
Point& upperLeft() const { return pData->ulhc;}
Point& lowerRight() const { return pData->lrhc;}
...
};
Такой вариант откомпилируется, но он неправильный! Фактически он внутренне противоречив. С одной стороны, upperLeft и lowerRight объявлены как константные функции-члены, поскольку они предназначены только для того, чтобы предоставить клиенту способ получить информацию о точках Rectangle, не давая ему возможности модифицировать объект Rectangle (см. правило 3). С другой стороны, обе функции возвращают ссылки на закрытые внутренние данные – ссылки, которые пользователь может затем использовать для модификации этих внутренних данных! Например:
Point coord1(0, 0);
Point coord2(100,100);
const Rectangle rec(coord1, coord2); // rec – константный прямоугольник
// от (0, 0) до (100, 100)
rec.upperLeft().setX(50); // теперь rec лежит между
// (50, 0) и (100, 100)!
Обратите внимание, что пользователь функции upperLeft может использовать возвращенную ссылку на один из данных-членов внутреннего объекта Point для модификации этого члена. Но ведь ожидается, что rec – константа!
Из этого примера следует извлечь два урока. Первый – член данных инкапсулирован лишь настолько, насколько доступна функция, возвращающая ссылку на него. В данном случае хотя ulhc и lrhc объявлены закрытыми, но на самом деле они открыты, потому что на них возвращают ссылки открытые функции upperLeft и lowerRight. Второй урок в том, что если константная функция-член возвращает ссылку на данные, ассоциированные с объектом, но хранящиеся вне самого объекта, то код, вызывающий эту функцию, может модифицировать данные. (Все это последствия ограничений побитовой константности – см. правило 3.)
Читать дальше
Конец ознакомительного отрывка
Купить книгу