Д. Вандевурд, Н.М. Джосаттис - Шаблоны C++. Справочник разработчика (2003) (1160769), страница 72
Текст из файла (страница 72)
Таким образом, копирование — неподходящая операция для хранителей обьектов. Вместо этого следует говорить, скорее, о передаче объектов от' одного хранителя другому. Операция передачи легко осуществляется с помощью операции освобождения объекта, за которой следует инициализация или присвоение. НОЫЕГ<ЯОВЕт)ЗЯПд> )т1(ПЕМ ЯОтЕс)ттнд); Но1с)ег<ЯомеГМпд> )з2()з1.ге1еаве()); Заметим еше раз, что синтаксис НОЫег<Х> )т = р; неприменим, поскольку влечет за собой неявное преобразование типов, в то время как конструктор объявлен как ехр11стт: Но1с)ег<Яотет)ттпд> )г2 = )з1.ге1еаве[); // ОШИБКА 20.1. Но!бег и Тгп!е 397 20.1.7. Копирование Но16ег при вызовах функций Явная передача работает вполне корректно, однако ситуация несколько усложняется, если передача осуществляется через границы вызова функций. Для случая передачи Но1с)ег от вызывающей функции в вызываемую всегда можно воспользоваться передачей по ссылке, а не по значению.
Использование освобождения с последующей инициализацией может привести к проблемам при передаче нескольких аргументов. НуС1авв х; са11ее(Ы.ге1еаве(),х); // Передача х может // сгенерировать исключение Если компилятор сначала вычислит Ы.ге1еаве(), то последующая передача х (в предположении, что осуществляется передача по значению), которая может привести к генерации исключения, вызовет утечку памяти, поскольку у объекта, хранившегося в Ь1, больше не будет владельца. Поэтому класс Но1с)ег всегда следует передавать только по ссылке. К сожалению, возврат хранителя объекта по ссылке далеко не всегда удобен, поскольку приводит к тому, что время жизни хранителя должно превышать вызов функции, а это, в свою очередь, не способствует ясному пониманию того, когда и как должен быть освобожден хранимый обьект.
Как вариант, можно вызвать функцию ге1еаве () непосредственно перед возвратом из функции. Рассмотрим следующий код: Яоюес)зепд* сгеасог() Но1г(ег<Яотек)зхпд> Ь(пеи Яоюес)зепд); МуС1авв х; // Введен с иллюстративной целью гесигп )з.ге1еаве()г ) В этом случае необходимо отдавать себе отчет, что деструкция х может вызвать исключение, которое генерируется после освобождения своего объекта хранителем )т и до того, как этот обьект получит вызывающая функция. В результате можно получить утечку ресурсов .13го еще раз подтверждает мысль о том, что генерация исключений в деструкторе — идея весьма нездоровая, особенно в том отношении, что генерация исключения в деструкторе в процессе развертывания стека при обработке предыдущего исключения приведет к немедленному завершению программы.) 20.1.8. Тгп1е Для решения проблем такого рода введем вспомогательный шаблон класса, предназначенный лля передачи хранителей.
Назовем его тги1е (от ггапзГег сараи!е — оболочка передачи) и определим, как показано ниже. // роепсегв/сги1е.)зрр ()халдей ТВАЙТ Е НРР Глава 20. Интеллектуальные указатели 398 йс)еййпе ТЫЛ Е НРР Гещр1аее сгурепаще Т> с1авв Но16егу сещр1аге сгурепаще Т> с1авв Тги1е ( ргфчаге: Т* ргг; // Объекты, с которыми работает класс ри)з11с: // Конструктор, гарантирующий, что класс используется // только как возвращаемый тип для передачи НоМег // вызывающей функции Тги1е(Но1с)егсТ>й )з) ( ргг = )з.дег() г )т.ге1еаве(); // Конструктор копирования Тги1е(Тги1есТ> сопвгй Г) ( рсг = г.рсг; сопвг савгстги1ест>й>(с).ргг = о; // Деструктор -Тги1е() ( с1е1есе ргг; ргйчаге: // Запрещено использование в качестве 1ча1ие // и копирующее присвоение Тги1е(Тги1есТ>й); Тги1есТ>й орегасог (Тги1есТ>й)г бг1епд с1авв НоЫегсТ>у ()еп61Й // Тй(Л Е НРР Очевидно, что конструктор копирования выглядит довольно безобразно.
Поскольку шаблон Тги1е предназначен для работы в качестве возвращаемого типа функции, его объекты всегда выступают в роли временных (гча!ие); следовательно, они могут оказаться ограниченными ссылкой на константный тип. Однако передача не может быть копированием и должна забрать владение объектом у исходного Тги1е, присвоив соответствующему указателю нулевое значение. Очеввщно, что это не константная операция. То, что сделано нами в конструкторе тги1е, выглядит безобразно, но вполне корректно, 20.1. Но1дег и Тго1е 399 если передаваемый объект на самом деле константным не является. Следовательно, при использовании шаблона Тги1е вы должны быть внимательны и объявлять возвращаемый тип функции как Тги1е<Т>, но не как Тги1е<Т> сопит. Заметим, что такого рода код при преобразовании Но1йег в Тги1е не используется: Но1йег должен быть неконстантным 1ча1ие.
Вот почему был использован отдельный тип для передачи вместо объединения необходимой нам функциональности в одном шаблоне с функциональностью Но1йег. Чтобы запретить использование Тги1е где бы то ни было помимо возвращаемого типа для передачи Но1йег, конструктор копирования, получающий ссылку на неконстантный объект, и аналогичный оператор присвоения объявлены закрытыми. Это позволяет избежать использования объектов Тги1е в качестве )ча1пе, но достаточной эта мера не является. Однако назначение этого шаблона — помочь программисту, а не помешать ополоумевшему хакеру. Шаблон Тги1е не будет полон до тех пор, пока не будет распознаваться шаблоном Но1йег. // ройпсегв/)то1йег2ехсг.)трр Сешр1аее <Сурепагее Т> с1авв Но1йег ( // Ранее определенные члены //.
ри)>11с: но1йег(тги1е<т> сопвса с) ( рсг = с.рсг; сопве саве<Тги1е<Т>а>(с).рсг = ОГ Но1йег<Т>а орегасог= (Тги1е<Т> сопвса с) йе1еее рсгг ,рсг = с.рсг; сопвс савс<тги1е<т>а>(е).рсг пг гесигп *с)тйв; Для иллюстрации взаимодействия пары Но1йег/Тги1е можно переписать наш пример Хоай вовек)тйпо () и вызвать ее.
ййпс1ийе >)то1йег2.)трр> ййпс1ийе "сги1е.)трр< с1авв ЯогаесМпй ( Глава 20. Интеллектуальные указателе иойс) геас) вомесЫпд(ЯоазесМпд* х) Тгц1е<Яомес)з|пд> 1оас) вомес)з1пд() ( Но1с)ег<Яоаеп)з1пд> гевц1П(пеи Яомеп)з1пд)з геас) вовек)зйпд(гевц1с.дес()); гесцгп гевц1с; Тпс тайп() ( Но1с)ег<яовес)зйпд> рсг(1оас) вощег)зйпд() ); //. В завершение следует отмеппь, что нами создана пара шаблонов классов, которые практически так же удобны в обращении, как и обычные указатели, но при этом обладают тем преимуществом, что при необходимости самостоятельно управляют освобождением объектов, в частности при развертывании стека в результате генерации исключения.
20.2. Счетчики ссылок Шаблон Но1с)ег и вспомогательный шаблон Тгц1е хорошо подходят для временного хранения структур в выделенной памяти, с тем чтобы в случае генерации исключения и развертывания стека эта память была автоматически освобождена. Однако утечки памяти могут проявляться и в другом контексте, в частности когда несколько объектов обьединены в одну сложную структуру. Общее правило управления динамически распределяемыми объектами гласит: если в приложении ничто не указывает на динамически распределенный объект, то такой обьект должен быть уничтожен, а его память должна быть доступна для повторного использования.
Таким образом, нет ничего удивительного в том, что программисты по мере сил и возможностей стремятся автоматизировать выполнение этого правила. Проблема состоит только в том, как определить, что ничто больше не указывает на объект. Одна идея, многократно реализованная на практике, состоит в использовании так называемого счетчика ссылок (ге1егепсе соцпбпя). Для каждого объекта, на который может иметься указатель, хранится количество таких указателей, и, когда оно падает до нуля. объект уничтожается. для того чтобы данная стратегия была осуществима в С++, слелу ет придерживаться определенных соглашений. В частности, поскольку непрактично отслеживать создание, копирование и уничтожение обычных указателей на объект, достаточно распространенным является требование использования для работы с объектом со счетчиком ссылок только интеллектуальных указателей определенного типа.
В этом разделе рассматривается реализация таких интеллектуальных указателей со счетчиками 20.2. Счетчики ссылок 401 ссылок. Такой указатель представляет собой шаблон, главным параметром которого яв- ляется тип объекта, на который он указывает. Сешр1асе<сурепаше Т ...> с1азз Соппе1пдркг ( рпЬ11с: // Конструктор, создающий новый счетчик для // указываемого объекта ехр11с1с Соипк1пдРСг(Т*)з // Копирование увеличивает значение счетчика Соипс1пдРСг(Соцпк1пдркг<Т ...> сопвсй); // Леструктор уменьшает значение счетчика 1п11пе -СоипкхпдРСг(); // Присвоение уменьшает значение счетчика для // ранее указанного объекта, и увеличивает // значение счетчика для присваиваемого объекта // (будьте осторожны с присвоением самому себе!) СоипгхпдРСг<Т ...>й орегагог=(Соипк1пдРсг<Т...> сопвгй) // Операторы интеллектуального указателя 1п11пе Тй орегаког* (); 1п11пе Т* орегаког->()з Параметр Т представляет собой единственный необходимый для построения функционируюшего шаблона счетчика ссылок параметр.
Хотя так и хочется оставить его единственным параметром с тем, чтобы шаблон был как можно более простым и надежным, мы выбрали Соипсйпдрег для демонстрации использования параметров стратегий (концепция, рассматриваемая в главе 15, "Классы свойств и стратегий"). Комментарии в приведенном фрапченте кода поясняют обший подход к счетчикам ссылок: каждый'Конструктор, деструктор и оператор присвоения могут потенциально изменить значение счетчика ссылок (когда значение счетчика достигнет нуля, объект удаляется). 20.2.1.
Где находится счетчик Поскольку наша идея состоит в подсчете количества указателей на объект, было бы совершенно логично разместить счетчик в объекте. К сожалению, зто не совсем удачная идея, если тип указываемого объекта не предусматривает такой счетчик. Если в самом обьекте счетчика ссылок нет, его можно разместить в отдельно выделенной области памяти, которая имеет, как минимум, ту же продолжительность жизни, что и указываемый объект; иными словами, эта область памяти должна быть динамически выделена. Применение для этого стандартного:: орегасог пем нз используемого компилятора обычно дает не самые лучшие результаты в плане производительности.
402 Глава 20. Интеллектуальные указате и Этот оператор должен быль способен выделять память под объекты произвольного Размера без существенных накладных расходов, что требует определенных вычислительных компромиссов. Таким образом, при реализации счетчиков ссылок гораздо более распространено использование специализированных распределителей памяти. Менее распространенной альтернативой раздельному выделению памяти для объекта и счетчика является использование распределителя памяти специального назначения, который может выделять дополнительную память для счетчика ссылок.