Выполняйте настройку явно и преднамеренно
Резюме
При разработке шаблона точки настройки должны быть написаны корректно, с особой тщательностью, а также ясно прокомментированы. При использовании шаблона необходимо четко знать, как именно следует настроить шаблон для работы с вашим типом, и выполнить соответствующие действия.
Обсуждение
Распространенная ловушка при написании библиотек шаблонов заключается в наличии непреднамеренных точек настройки, т.е. точек в вашем шаблоне, где может выполняться поиск пользовательского кода и его использование, но при написании такие действия вами не подразумевались. Попасть в такую ловушку очень легко — достаточно просто вызвать другую функцию или оператор обычным путем (без полной его квалификации), и если окажется, что один из его аргументов имеет тип параметра шаблона (или связанный с ним), то будет начат поиск такого кода, зависящий от аргумента. Примеров тому множество; в частности, см. рекомендацию 58. Поэтому лучше использовать такие точки преднамеренно. Следует знать три основных пути обеспечения точек настройки в шаблоне, решить, какой именно способ вы хотите использовать в данном месте шаблона, и корректно его закодировать. Затем проверьте, не осталось ли в вашем коде случайных точек настройки там, где вы не предполагали их наличие. Первый способ создания точки настройки — обычный "неявный интерфейс" (см. рекомендацию 64), когда ваш шаблон просто рассчитывает на то, что тип имеет соответствующий член с данным именем: // Вариант 1. Создание точки настройки путем требования от // типа T "foo-совместимости", т.е. наличия функции-члена с // данным именем, сигнатурой и семантикой template<typename T> void Sample1(T t) { t.foo(); // foo - точка настройки typename T::value_type x; // Еще один пример: создание } // точки настройки для поиска // типа (обычно создается посредством typedef) Для реализации первого варианта автор Sample1 должен выполнить следующие действия. • Вызвать функцию как член. Просто используйте естественный синтаксис вызова функции-члена. • Документировать точку настройки. Тип должен обеспечить доступную функцию-член foo, которая может быть вызвана с данными аргументами (в данном случае — без аргументов). Второй вариант представляет собой использование метода "неявного интерфейса", но с функциями, не являющимися членами, поиск которых выполняется с использованием ADL[3](т.е. ожидается, что данная функция находится в пространстве имен типа, для которого выполняется инстанцирование шаблона). Именно эта ситуация и явилась основной побудительной причиной для введения ADL (см. рекомендацию 57). Ваш шаблон рассчитывает на то, что для используемого типа имеется подходящая функция с заданным именем: // Вариант 2: Создание точки настройки путем требования от // типа T "fоо-совместимости", т.е. наличия функции, не // являющейся членом с данным именем, сигнатурой и // семантикой, поиск которой выполняется посредством ADL. // (Это единственный вариант, при котором не требуется поиск // самого типа T.) template<typename T> void Samplе2(T t) { foo(t); // foo - точка настройки cout << t; // Еще один пример - operator<< с записью в } // виде оператора представляет собой такую же // точку настройки Для реализации варианта 2 автор Samplе2 должен выполнить следующие действия. • Вызвать функцию с использованиемнеквалифицированного имени (включая использование естественного синтаксиса в случае операторов) и убедиться, что шаблон не имеет функции-члена с тем же именем. В случае шаблонов очень важно, чтобы вызов функции был не квалифицированным (например, не следует писать SomeNamespace::foo(t)) и чтобы у шаблона не было функции-члена с тем же именем, поскольку в обоих этих случаях поиск, зависящий от аргумента, выполняться не будет, что предотвратит поиск имени в пространстве имен, в котором находится тип T. • Документировать точку настройки. Тип должен обеспечить наличие функции, не являющейся членом, которая может быть вызвана с данными аргументами. Варианты 1 и 2 имеют одинаковые преимущества и применимость: пользователь может один раз написать соответствующую функцию настройки для своего типа и разместить ее там, где ее смогут найти и шаблоны других библиотек. Тем самым пользователь избегает необходимости писать множество мелких адаптеров для каждой библиотеки отдельно. Недостаток же заключается в том, что соответствующая семантика должна быть достаточно широко применима и иметь смысл для всех такого рода потенциальных применений (заметим, что в частности в эту категорию попадают операторы, что является еще одной причиной для рекомендации 26). Третий вариант заключается в использовании специализации, когда ваш шаблон полагается на то, что пользовательский тип специализирует (при необходимости) некоторый иной предоставленный вами шаблон класса. // Вариант 3: Создание точки настройки путем требования от // типа T "foo-совместимости" путем специализации шаблона // SampleTraits<> с предоставлением (обычно статической) // функции с данным именем, сигнатурой и семантикой. template<typename T> void Samplе3(T t) { S3Traits<T>::foo(t); // S3Traits<>::foo - // точка настройки typename S3Traits<T>::value_type x; // Другой пример - } // точка настройки для поиска типа (обычно // создается посредством typedef) В этом варианте пользователь пишет адаптер, который гарантирует изолированность кода настройки для данной библиотеки в пределах этой библиотеки. Соответствующий недостаток заключается в том, что это может оказаться слишком громоздким решением; если несколько библиотек шаблонов требуют одну и ту же общую функциональность, пользователь должен будет писать несколько адаптеров, по одному для каждой библиотеки. Для реализации этой версии автор Samplе3 должен выполнить следующие действия. • Предоставить шаблон класса по умолчанию в собственном пространстве имен шаблона. Не используйте шаблоны функций, которые нельзя частично специализировать и которые приводят к перегрузкам и зависимостям от порядка (см. также рекомендацию 66). • Документировать точку настройки. Пользователь должен специализировать S3Traits для своего собственного типа в пространстве имен библиотеки шаблонов, и документировать все члены S3Traits (например, foo) и их семантику. При использовании любого из перечисленных вариантов следует также четко документировать семантику, требуемую от foo, в особенности все существенные действия (постусловия), которые должна гарантировать эта функция, и семантику сбоев (что именно происходит при сбое и каким образом должно осуществляться оповещение об ошибках). Если точка настройки должна действовать и для встроенных типов, используйте варианты 2 и 3. Варианты 1 и 2 следует предпочесть для тех общих операций, которые являются предоставляемыми типом сервисами. Для принятия данного решения попробуйте ответить на следующие вопросы: могут ли другие библиотеки шаблонов использовать данную возможность? является ли рассматриваемая семантика приемлемой для данного имени в общем случае? Если вы положительно ответили на эти вопросы, то, вероятно, вам действительно следует предпочесть один из этих вариантов. Вариант 3 лучше использовать для менее общих операций, смысл которых может варьироваться. В таком случае в другом пространстве имен без каких-либо коллизий вы сможете придать тому же имени иной смысл. Шаблон, в котором имеется несколько точек настройки, для каждой из них может выбрать свою стратегию, в наибольшей мере приемлемую в данном месте. Главное, что вы должны осознанно, с пониманием выбирать стратегию для каждой точки настройки, документировать требования к настройке (включая ожидаемые постусловия и семантику ошибок) и корректно реализовать выбранную вами стратегию. Для того чтобы избежать непреднамеренных точек настройки, следует придерживаться следующих правил. • Размещайте все используемые вашим шаблоном вспомогательные функции в их собственном вложенном пространстве имен, и вызывайте их посредством полностью квалифицированных имен для запрета ADL. Если вы вызываете вашу вспомогательную функцию и передаете ей объект типа параметра шаблона, и этот вызов не должен быть точкой настройки (т.е. вы всегда намерены вызывать вашу вспомогательную функцию, а не некоторую иную), то лучше поместить эту вспомогательную функцию во вложенное пространство имен и явно запретить ADL, полностью квалифицировав имя вызываемой функции или взяв его в скобки: template<typename T> void Samplе4(T t) { S4Helpers::bar(t); // Запрет ADL: bar не является // точкой настройки (bar)(t); // Альтернативный способ } • Избегайте зависимости от зависимых имен. Говоря неформально, зависимое имя — это имя, которое каким-то образом упоминает параметр шаблона. Многие компиляторы не поддерживают "двухфазный поиск" для зависимых имен из стандарта С++, а это означает, что код шаблона, использующий зависимые имена, будет вести себя по-разному на разных компиляторах, если только не принять меры для полной определенности при использовании зависимых имен. В частности, особого внимания требует наличие зависимых базовых классов, когда шаблон класса наследуется от одного из параметров этого шаблона (например, T в случае template<typename T>class С:T{};) или от типа, который построен с использованием одного из параметров шаблона (например, X<T> в случае template<typename T>class C:X<T>{};). Коротко говоря, при обращении к любому члену зависимого базового класса необходимо всегда явно квалифицировать имя с использованием имени базового класса или при помощи this->. Этот способ можно рассматривать просто как некую магию, которая заставляет все компиляторы делать именно то, что вы от них хотите. template<typename T> class С : X<T> { typename X<T>::SomeType s; // Использование вложенного // типа (или синонима // typedef) из базового // класса public: void f() { X<T>::baz(); // вызов функции-члена // базового класса this->baz(); // Альтернативный способ } }; Стандартная библиотека С++ в основном отдает предпочтение варианту 2 (например, ostream_iterator ищет оператор operator<<, a accumulate ищет оператор operator+ в пространстве имен вашего типа). В некоторых местах стандартная библиотека использует также вариант 3 (например, iterator_traits, char_traits) в основном потому, что эти классы свойств должны быть специализируемы для встроенных типов. Заметим, что, к сожалению, стандартная библиотека С++ не всегда четко определяет точки настройки некоторых алгоритмов. Например, она ясно говорит о том, что трехпараметрическая версия accumulate должна вызывать пользовательский оператор operator+ с использованием второго варианта. Однако она не говорит, должен ли алгоритм sort вызывать пользовательскую функцию swap (обеспечивая таким образом преднамеренную точку настройки с использованием варианта 2), может ли он использовать пользовательскую функцию swap, и вызывает ли он функцию swap вообще; на сегодняшний день некоторые реализации sort используют пользовательскую функцию swap, в то время как другие реализации этого не делают. Важность рассматриваемой рекомендации была осознана совсем недавно, и сейчас комитет по стандартизации исправляет ситуацию, устраняя такие нечеткости из стандарта. Не повторяйте такие ошибки. (См. также рекомендацию 66.)
Ссылки
[Stroustrup00] §8.2, §10.3.2, §11.2.4 • [Sutter00] §31-34 • [Sutter04d]
Популярное: Как распознать напряжение: Говоря о мышечном напряжении, мы в первую очередь имеем в виду мускулы, прикрепленные к костям ... Почему люди поддаются рекламе?: Только не надо искать ответы в качестве или количестве рекламы... Модели организации как закрытой, открытой, частично открытой системы: Закрытая система имеет жесткие фиксированные границы, ее действия относительно независимы... ©2015-2024 megaobuchalka.ru Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. (389)
|
Почему 1285321 студент выбрали МегаОбучалку... Система поиска информации Мобильная версия сайта Удобная навигация Нет шокирующей рекламы |