Правило 15: Предоставляйте доступ к самим ресурсам из управляющих ими классов
Правило 15: Предоставляйте доступ к самим ресурсам из управляющих ими классов
Управляющие ресурсами классы заслуживают всяческих похвал. Это бастион, защищающий от утечек ресурсов, а отсутствие таких утечек – фундаментальное свойство хорошо спроектированных систем. В идеальном мире вы можете положиться на эти классы для любых взаимодействий с ресурсами, не утруждая себя доступом к ним напрямую. Но мир неидеален. Многие программные интерфейсы требуют доступа к ресурсам без посредников. Если вы не планируете отказаться от использования таких интерфейсов (что редко имеет смысл на практике), то должны как-то обойти управляющий объект и работать с самим ресурсом.
Например, в правиле 13 изложена идея применения интеллектуальных указателей вроде auto_ptr или tr1::shared_ptr для хранения результата вызова фабричной функции createInvestment:
std::tr1::shared_ptr<Investment> pInv(createInvestment()); // ?? ??????? 13
Предположим, есть функция, которую вы хотите применить при работе с объектами класса Investment:
int daysHeld(const Investment *pi); // возвращает количество дней
// хранения инвестиций
Вы хотите вызывать ее так:
int days = daysHeld(pInv); // ошибка!
но этот код не скомпилируется: функция daysHeld ожидает получить указатель на объект класса Investment, а вы передаете ей объект типа tr1::shared_ptr <Investment>.
Необходимо как-то преобразовать объект RAII-класса (в данном случае tr1::shared_ptr) к типу управляемого им ресурса (то есть Investment*). Есть два основных способа сделать это: неявное и явное преобразование.
И tr1::shared_ptr, и auto_ptr предоставляют функцию-член get для выполнения явного преобразования, то есть возврата (копии) указателя на управляемый объект:
int days = daysHeld(pInv.get()); // нормально, указатель, хранящийся
// в pInv, передается daysHeld
Как почти все классы интеллектуальных указателей, tr1::shared_ptr и auto_ptr перегружают операторы разыменования указателей (operator-> и operator*), и это обеспечивает возможность неявного преобразования к типу управляемого указателя:
class Investment { // корневой класс иерархии
public: // типов инвестиций
bool isTaxFree() const;
...
};
Investment *createInvestment(); // фабричная функция
std::tr1::shared_ptr<Investment> // имеем tr1::shared_ptr
pi1(createInvestment()); // для управления ресурсом
bool taxable1 = !(pi1->isTaxFree()); // доступ к ресурсу
// через оператор ->
...
std::auto_ptr<Investment> pi2(createInvestment()); // имеем auto_ptr для
// управления ресурсом
bool taxable2 = !((*pi2).isTaxFree()); // доступ к ресурсу
// через оператор *
...
Поскольку иногда необходимо получать доступ к ресурсу, управляемому RAII-объектом, то некоторые реализации RAII предоставляют функции для неявного преобразования. Например, рассмотрим следующий класс для работы со шрифтами, инкапсулирующий «родной» интерфейс, написанный на C:
FontHandle getFont(); // из С API – параметры пропущены
// для простоты
void releaseFont(FontHandle fh); // из того же API
class Font { // класс RAII
public:
explicit Font(FontHandle fh) // захватить ресурс:
:f(fh) // применяется передача по значению,
{} // потому что того требует C API
~Font() {releaseFont(f);} // освободить ресурс
private:
FontHandle f; // управляемый ресурс – шрифт
};
Предполагается, что есть обширный программный интерфейс, написанный на C, работающий исключительно в терминах FontHandle. Поэтому часто приходится преобразовывать объекты из типа Font в FontHandle. Класс Font может предоставить функцию явного преобразования, например get:
class Font {
public:
...
FontHandle get() const {return f;} // функция явного преобразования
...
};
К сожалению, пользователю придется вызывать get всякий раз при взаимодействии с API:
void changeFontSize(FontHandle f, int newSize); // из C API
Font f(getFont());
int newFontSize;
...
changeFontSize(f.get(), newFontSize); // явное преобразование
// из Font в FontHandle
Некоторые программисты могут посчитать, что каждый раз выполнять явное преобразование настолько обременительно, что вообще откажутся от применения этого класса. В результате возрастет опасность утечки шрифтов, а именно для того, чтобы предотвратить это, и был разработан класс Font.
Альтернативой может стать предоставление классом Font функции неявного преобразования к FontHandle:
class Font {
public:
...
operator FontHandle() const // функция неявного преобразования
{return f;}
...
};
Это сделает вызовы C API простыми и естественными:
Font f(getFont());
int newSize;
...
changeFontSize(f, newFontSize); // неявное преобразование из Font
// в FontHandle
Увы, у этого решения есть и оборотная сторона: повышается вероятность ошибок. Например, пользователь может нечаянно создать объект FontHandle, имея в виду Font:
Font f1(getFont());
...
FontHandle f2 = f1; // Ошибка! Предполагалось скопировать объект Font,
// а вместо f1 неявно преобразован в управляемый
// им FontHandle, который и скопирован в f2
Теперь в программе есть FontHandle, управляемый объектом Font f1, однако он же доступен и напрямую, как f2. Это почти всегда нехорошо. Например, если f1 будет уничтожен, шрифт освобождается, и f2 становится «висячей ссылкой».
Решение о том, когда нужно предоставить явное преобразование RAII-объекта к управляемому им ресурсу (посредством функции get), а когда – неявное, зависит от конкретной задачи, для решения которой был спроектирован класс, и условий его применения. Похоже, что лучшее решение – следовать советам правила 18, а именно: делать интерфейсы простыми для правильного применения и трудными – для неправильного. Часто явное преобразование типа функции get – более предпочтительный вариант, поскольку минимизирует шанс получить нежелательное преобразование типов. Однако иногда естественность применения неявного преобразования поможет сделать ваш код чище.
Может показаться, что функции, обеспечивающие доступ к управляемым ресурсам, противоречат принципам инкапсуляции. Верно, но в данном случае это не беда. Дело в том, что RAII-классы существуют не для того, чтобы что-то инкапсулировать. Их назначение – гарантировать, что определенное действие (а именно освобождение ресурса) обязательно произойдет. При желании инкапсуляцию ресурса можно реализовать поверх основной функциональности, но это не является необходимым. Более того, некоторые RAII-классы комбинируют истинную инкапсуляцию реализации с отказом от нее в отношении управляемого ресурса. Например, tr1::shared_ptr инкапсулирует подсчет ссылок, но предоставляет простой доступ к управляемому им указателю. Как и большинство хорошо спроектированных классов, он скрывает то, что клиенту не нужно видеть, но обеспечивает доступ к тому, что клиенту необходимо.
Что следует помнить
• Программные интерфейсы (API) часто требуют прямого обращения к ресурсам. Именно поэтому каждый RAII-класс должен предоставлять возможность получения доступа к ресурсу, которым он управляет.
• Доступ может быть обеспечен посредством явного либо неявного преобразования. Вообще говоря, явное преобразование безопаснее, но неявное более удобно для пользователей.
Данный текст является ознакомительным фрагментом.