Категории
Самые читаемые
Лучшие книги » Компьютеры и Интернет » Программирование » Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Читать онлайн Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 16 17 18 19 20 21 22 23 24 ... 73
Перейти на страницу:

К счастью, tr1::shared_ptr позволяет задать «чистильщика» – функцию или функциональный объект, который должен быть вызван, когда счетчик ссылок достигает нуля (эта функциональность не предусмотрена для auto_ptr, который всегда удаляет указатель). Функция-чистильщик – это необязательный второй параметр конструктора tr1::shared_ptr, поэтому код должен выглядеть так:

class Lock {

public:

explicit Lock(Mutex *pm) // инициализировать shared_ptr объектом

: mutexPtr(pm, unlock) // Mutex, на который он будет

// указывать, функцией unlock

{ // в качестве чистильщика

lock(mutexPtr.get());

}

private:

std::tr1::shared_ptr<Mutex> mutexPtr; // использовать

}; // shared_ptr вместо

// простого указателя

Отметим, что в этом примере в классе Lock больше нет деструктора. Просто в нем отпала необходимость. В правиле 5 объясняется, что деструктор класса (независимо от того, сгенерирован он компилятором или определен пользователем) автоматически вызывает деструкторы нестатических данных-членов класса. В нашем примере это mutexPtr. Но деструктор mutexPtr автоматически вызовет функцию-чистильщик tr1::shared_ptr (в данном случае unlock), когда счетчик ссылок на мьютекс достигнет нуля. (Пользователи, которые будут знакомиться с исходным текстом класса, вероятно, будут благодарны за комментарии, указывающие, что вы не забыли о деструкторе, а просто положились на поведение по умолчанию деструктора, сгенерированного компилятором.)

Копирование управляемого ресурса. Иногда допустимо иметь столько копий ресурса, сколько вам нужно, и единственная причина использования класса, управляющего ресурсами, – гарантировать, что каждая копия ресурса будет освобождена по окончании работы с ней. В этом случае копирование управляющего ресурсом объекта означает также копирование самого ресурса, который в него «обернут». То есть копирование управляющего ресурсом объекта выполняет «глубокое копирование». Некоторые реализации стандартного класса string включают указатели на память из «кучи», где хранятся символы, входящие в строку. Объект такого класса содержит указатель на память из «кучи». Когда объект string копируется, то копируется и указатель, и память, на которую он указывает. Здесь мы снова встречаемся с «глубоким копированием».

• Передача владения управляемым ресурсом. Иногда нужно гарантировать, что только один RAII-объект ссылается на ресурс, и при копировании такого объекта RAII владение ресурсом передается объекту-копии. Как объясняется в правиле 13, это означает копирование с применением auto_ptr.

Копирующие функции (конструктор копирования и оператор присваивания) могут быть сгенерированы компилятором, но если сгенерированные версии не делают того, что вам нужно (правило 5 объясняет поведение по умолчанию), придется написать их самостоятельно. Иногда имеет смысл поддерживать обобщенные версии этих функций. Такой подход описан в правиле 45.

Что следует помнить

• Копирование RAII-объектов влечет за собой копирование ресурсов, которыми они управляют, поэтому поведение ресурса при копировании определяет поведение RAII-объекта.

• Обычно при реализации RAII-классов применяется одна из двух схем: запрет копирования или подсчет ссылок, но возможны и другие варианты.

Правило 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 – более предпочтительный вариант, поскольку минимизирует шанс получить нежелательное преобразование типов. Однако иногда естественность применения неявного преобразования поможет сделать ваш код чище.

1 ... 16 17 18 19 20 21 22 23 24 ... 73
Перейти на страницу:
На этой странице вы можете бесплатно скачать Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс торрент бесплатно.
Комментарии