|
Программирование >> Обобщенные обратные вызовы
аргументом destroy(T*), передавая ей один из итераторов. Это автоматически приводит к тому требованию, что итератор Fwditer должен быть обычным указателем! А это означает ненужную потерю общности. У Рекомендация Запомните, что указатели (в массив) всегда являются итераторами, но итераторы не всегда представляют собой указатели. Это также означает, что вы можете получить весьма туманные сообщения об ошибках при попытке скомпилировать код, который пытается осуществить вызов de-stroyCFwdlter, Fwditer) с итераторами, не являющимися указателями, поскольку ошибка будет диагностироваться в строке destroy(fi rst). Выводимые при этом сообщения об ошибках, скажем прямо, особой ясности не прибавляют. Посмотрите, как они выглядят у одного из pacnpocTpaHChHHbix компиляторов. void cdecl destroy(temp late parameter -1, template-parameter 1) : expects 2 arguments - 1 provided void cdecl destroy(tempiate-parameter-l *) : could not deduce template argument for tempiate-parameter-l * from [использованный тип итератора] Это не худшие из виденных мною сообщений об ошибках, и в принципе не так сложно понять, что именно они обозначают. Первое сообщение указывает, что компилятор пытался разрешить вызов destroyCfi rst); как вызов версии destroy с двумя аргументами. Второе говорит о попытке вызова версии с одним параметром. Обе попытки оказались неудачными, каждая по своей причине. Версию с двумя аргументами устраивает тип итератора, но ей нужны два параметра, а не один. Версия же с одним параметром требует, чтобы он обязательно был указателем. Не везет! В действителыюсти, мы вряд ли когда-то будем использовать функцию destroy с чем-либо кроме указателей в силу се предназначения - превратить объекты в обычную память. Тем не менее, одно небольшое изменение - и Fwditer может быть итератором произвольного типа, а не только указателем. Так почему бы не сделать его? Все, что надо сделать, - это в версии с дву.мя параметрами заменить вызов destroyC fi rst ); вызовом destroyC &*fi rst ); Такая замена будет работать практически всегда. Здесь мы разыменовываем итератор для того, чтобы получить непосредственную ссылку на содержащийся в контейнере объект, а затем получаем его адрес, что дает нам требуемый указатель. В соответствии со стандартом, все итераторы должны иметь оператор *, который возвращает истинную ссылку т&. Это одна из причин, по которой стандартом С++- не поддерживаются прокси-контейнеры (дополнительную информацию по этому вопросу можно найти в книге [Su!ter02]. (Возможны, хотя и крайне редки, ситуации, когда вызов destroy(& f i rst); будет работать некорректно: как указал хитроумный читатель Билл Вейд (Bill Wade), этот метод неприменим, если в т перегружен оператор &, который возвраиыст нечто вместо адреса объекта. Но это уже патология, и оправдания такому дизайну я просто не в состоянии придумать.) В чем же мораль этой истории? Не забывайте о возможных нарушениях универсальности при реализации одной обобщенной функции с использованием другой. В нашем случае это вылилось в то, что версия функции с двумя параметрами оказалась не Задача 1.13 в издании на русском языке. - Прим. ред. 46 Обобщенное программирование и стандартная библиотека С++ настолько универсальной в плане использования итераторов, как мы изначально рассчитывали. Более того, в нашей задаче мы сталкиваемся с еще одним препятствием: попытка исправить ситуацию путем замены destroy(fi rst); вызовом destroy (&fi rst); приводит к новому требованию к типу т. которое заключается в том, что он должен предоставлять оператор & с обычной семантикой, т.е. возвращающий указатель на объект. Обеих ловушек можно избежать, если не использовать одну функцию в реализации другой. Примечание. Я не призываю вас полностью отказаться от реализации шаблонов с использованием шаблонов; но помните о проблемах, которые может вызвать такое взаимодействие. Очевидно, что шаблоны часто могут быть реализованы при помоши других шаблонов. Например, программисты часто специализируют шаблон std: : swap для своих типов, если они знают более .эффективный способ обмена двух значений. И если вы пишете шаблон сортировки, то эта сортировка должна быть реализована с использованием вызова шаблона swap - в противном случае ваш шаблон не сможет воспользоваться оптимизированным способом обмена содержимого объектов. 2. В чем заключается семантика приведенной далее функции, включая требования к т? Можно ли снять какие-либо из этих требований? Если да, то проде.монстрируйте, как именно, и укажите достоинства и недостатки соответствующего решения. Пример 6-2(а) template <class т> void swap С т& а, т& b ) { т temp(a); а = Ь; Ь = temp; Шаблон функции swap просто обменивает два значения с использованием копирующего конструктора и оператора копирующего присваивания. Таким образом, этот шаблон требует, чтобы тип т имел конструктор копирования и оператор копирующего присваивания. Если это все, что вы смогли сказать, - поставьте себе за этот ответ только полбалла. Одной из важнейших составляющих семантики любой функции является ее безопасность с точки зрения исключений, включая обеспечиваемые сю гарантии безопасности. В данном случае функция swap не является безопасной в смысле исключений, если оператор копирующего присваивания Т может генерировать исключения. В частности, если Т::operators может генерировать исключения, но при этом атомарен ( все или ничего ), то если второе присваивание оказалось некорректным, мы выходим из функции по исключению, но при этом объект а оказывается уже измененным. Если же оператор т; :operator= может генерировать исключения и не является атомарным, то возможна ситуация, когда мы выходим из функции по исключению и при этом оба параметра оказываются измененными (и один из них может не содержать ни одного из двух исходных значений). Следовательно, данный шаблон функции swap должен быть документирован следующим образом. Если оператор т: :operator= не генерирует исключений, swap гарантирует атомарность операции ( все или ничего ), за исключением побочных действий операций т (см. также [Sutter99]). В противном случае, если оператор т: :operator= может генерировать исключение : если оператор т: :operator= атомарен и осуществляется выход из функции swap по исключению, первый аргумент функции может быть изменен (хотя и не обязательно); в противном случае, если оператор T::operator= не атомарен и осуществляется выход из функции swap по исключению, оба аргумента могут быть изменены (хотя и не обязательно), причем один из них может не содержать ни первое, ни второе исходное значение. Существует два способа избавиться от требования, касающиеся типа т и состоящего в том, что т должен иметь оператор присваивания, причем первый способ обеспечивает большую безопасность исключений. /. Специализация или перегрузка swap. Пусть, например, у нас есть класс MyClass, который использует распространенную идиому не генерирующей исключений функции Swap. Тогда мы може.м специализировать стандартную функцию для MyClass следующим образом. пример 6-2(6): Специализация swap. class MyClass { public: void swap( MyClassA ) /* throwO */ ; namespace std { tempiateo void swap<MyClass>(Myc1ass& a, MyClass& b ) { throwO a.Swap( b ); Мы можем также перегрузить стандартную функцию для MyClass следующим образом: пример б-2(в): перегрузка swap. class MyClass { public: void Swap( Myclass& ) /* throwO */ ; ... примечание: не в пространстве имен std. void swapC MyClass& a, MyClass& b ) /* throw() */ { a.SwapC D ); Обычно это неплохая мысль - даже если т имеет оператор присваивания, который обеспечивает корректную работу исходной версии кода! Например, стандартная библиотека перегружает** swap для векторов, так что вызов swap приводит к вызову vector:: swap. Это имеет большой смысл, поскольку обмен vector: ; swap становится более эффективным, если удается избежать копирования данных, содержащихся в векторах. Первичный шаблон в примере 6-2(а) создает новую копию (temp) одного из векторов, а затем выполняет дополнительное копирование из одного вектора в другой и из вектора temp во второй вектор, что приводит к массе операций т и времени работы 0(N), где N - общий размер обмениваемых векторов. Специализированная версия обычно просто выполняет присваивание нескольких указателей и целочисленных объектов, причем в течение постоянного (и обычно пренебрежимо малого) времени. Так что если вы создаете тип, в котором есть операция наподобие swap, имеет смысл специализировать std:: swap (или предоставить свою собственную функцию swap в другом просфанствс имен) для вашего типа. Обычно это оказывается более эффективным способом обмена, чем работающий в лоб шаблон std: iswap из стан- Но не специализирует , так как вы не можете частично специализировать шаблоны функций. См. задачу 7, в которой более подробно рассматриваются шаблоны функций и специализации.
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |