Главная страница
Библиотека (скачать книги)
Скачать софт
Введение в программирование
Стандарты для C++
Уроки по C#
Уроки по Python
HTML
Веб-дизайн
Ассемблер в среде Windows
ActiveX
Javascript
Общее о Линукс
Линукс - подробно
Линукс - новое
Delphi
Паскаль для начинающих
Турбопаскаль
Новости
Партнеры
Наши предложения
Архив новостей







65. Выполняйте настройку явно и преднамеренно

Резюме

При разработке шаблона точки настройки должны быть написаны корректно, с особой тщательностью, а также ясно прокомментированы. При использовании шаблона необходимо четко знать, как именно следует настроить шаблон для работы с вашим типом, и выполнить соответствующие действия.

Обсуждение

Распространенная ловушка при написании библиотек шаблонов заключается в наличии непреднамеренных точек настройки, т.е. точек в вашем шаблоне, где может выполняться поиск пользовательского кода и его использование, но при написании такие действия вами не подразумевались. Попасть в такую ловушку очень легко — достаточно просто вызвать другую функцию или оператор обычным путем (без полной его квалификации), и если окажется, что один из его аргументов имеет тип параметра шаблона (или связанный с ним), то будет начат поиск такого кода, зависящий от аргумента. Примеров тому множество; в частности, см. рекомендацию 58.

Поэтому лучше использовать такие точки преднамеренно. Следует знать три основных пути обеспечения точек настройки в шаблоне, решить, какой именно способ вы хотите использовать в данном месте шаблона, и корректно его закодировать. Затем проверьте, не осталось ли в вашем коде случайных точек настройки там, где вы не предполагали их наличие.

Первый способ создания точки настройки — обычный "неявный интерфейс" (см. рекомендацию 64), когда ваш шаблон просто рассчитывает на то, что тип имеет соответствующий член с данным именем:

// Вариант 1. Создание точки настройки путем требования от
// типа T "foo-совместимости", т.е. наличия функции-члена с
// данным именем, сигнатурой и семантикой
template<typename T>
void Sample1( T t ) {
    t.foo();                  // foo - точка настройки
    typename T::value_type x; // Еще один пример: создание
}                             // точки настройки для поиска
             // типа (обычно создается посредством typedef)

Для реализации первого варианта автор Sample1 должен выполнить следующие действия.

  • Вызвать функцию как член. Просто используйте естественный синтаксис вызова функции-члена.

  • Документировать точку настройки. Тип должен обеспечить доступную функцию-член foo, которая может быть вызвана с данными аргументами (в данном случае — без аргументов).

Второй вариант представляет собой использование метода "неявного интерфейса", но с функциями, не являющимися членами, поиск которых выполняется с использованием ADL3 (т.е. ожидается, что данная функция находится в пространстве имен типа, для которого выполняется инстанцирование шаблона). Именно эта ситуация и явилась основной побудительной причиной для введения ADL (см. рекомендацию 57). Ваш шаблон рассчитывает на то, что для используемого типа имеется подходящая функция с заданным именем:

// Вариант 2: Создание точки настройки путем требования от
// типа T "foo-совместимости", т.е. наличия функции, не
// являющейся членом с данным именем, сигнатурой и
// семантикой, поиск которой выполняется посредством ADL.
// (Это единственный вариант, при котором не требуется поиск
// самого типа T.)
template<typename T>
void Sample2( T t ) {
    foo( t );    // foo - точка настройки
    cout << t;   // Еще один пример - operator<< с записью в
}                // виде оператора представляет собой такую же
                 // точку настройки

Для реализации варианта 2 автор Sample2 должен выполнить следующие действия.

  • Вызвать функцию с использованием неквалифицированного имени (включая использование естественного синтаксиса в случае операторов) и убедиться, что шаблон не имеет функции-члена с тем же именем. В случае шаблонов очень важно, чтобы вызов функции был не квалифицированным (например, не следует писать 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) и их семантику.

При использовании любого из перечисленных вариантов следует также четко документировать семантику, требуемую от f оо, в особенности все существенные действия (постусловия), которые должна гарантировать эта функция, и семантику сбоев (что именно происходит при сбое и каким образом должно осуществляться оповещение об ошибках).

Если точка настройки должна действовать и для встроенных типов, используйте варианты 2 и 3.

Варианты 1 и 2 следует предпочесть для тех общих операций, которые являются предоставляемыми типом сервисами. Для принятия данного решения попробуйте ответить на следующие вопросы: могут ли другие библиотеки шаблонов использовать данную возможность? является ли рассматриваемая семантика приемлемой для данного имени в общем случае? Если вы положительно ответили на эти вопросы, то, вероятно, вам действительно следует предпочесть один из этих вариантов.

Вариант 3 лучше использовать для менее общих операций, смысл которых может варьироваться. В таком случае в другом пространстве имен без каких-либо коллизий вы сможете придать тому же имени иной смысл.

Шаблон, в котором имеется несколько точек настройки, для каждой из них может выбрать свою стратегию, в наибольшей мере приемлемую в данном месте. Главное, что вы должны осознанно, с пониманием выбирать стратегию для каждой точки настройки, документировать требования к настройке (включая ожидаемые постусловия и семантику ошибок) и корректно реализовать выбранную вами стратегию.

Для того чтобы избежать непреднамеренных точек настройки, следует придерживаться следующих правил.

  • Размещайте все используемые вашим шаблоном вспомогательные функции в их собственном вложенном пространстве имен, и вызывайте их посредством полностью квалифицированных имен для запрета ADL. Если вы вызываете вашу вспомогательную функцию и передаете ей объект типа параметра шаблона, и этот вызов не должен быть точкой настройки (т.е. вы всегда намерены вызывать вашу вспомогательную функцию, а не некоторую иную), то лучше поместить эту вспомогательную функцию во вложенное пространство имен и явно запретить ADL, полностью квалифицировав имя вызываемой функции или взяв его в скобки:

    template<typename т>
    void  Sample4( T t ) {
        S4Helpers::bar( t );   // Запрет ADL: bar не является
                               // точкой настройки
        (bar)( t );            // Альтернативный способ
    }
    
  • Избегайте зависимости от зависимых имен. Говоря неформально, зависимое имя — это имя, которое каким-то образом упоминает параметр шаблона. Многие компиляторы не поддерживают "двухфазный поиск" для зависимых имен из стандарта C++, а это означает, что код шаблона, использующий зависимые имена, будет вести себя по-разному на разных компиляторах, если только не принять меры для полной определенности при использовании зависимых имен. В частности, особого внимания требует наличие зависимых базовых классов, когда шаблон класса наследуется от одного из параметров этого

    шаблона (например, Т в случае template<typename T>class C:T{};) или от типа, который построен с использованием одного из параметров шаблона (например, Х<Т> в случае template<typename T>class C:X<T>{};).

Коротко говоря, при обращении к любому члену зависимого базового класса необходимо всегда явно квалифицировать имя с использованием имени базового класса или при помощи this->. Этот способ можно рассматривать просто как некую магию, которая заставляет все компиляторы делать именно то, что вы от них хотите.

template<typename T>
class С : Х<Т> {
    typename Х<Т>::SomeType s;  // использование вложенного
                                // типа (или синонима
                                // typedef) из базового
                                // класса
public:
    void f() {
        X<T>::baz();            // Вызов функции-члена
                                // базового класса
        this->baz();            // Альтернативный способ
};

Стандартная библиотека C++ в основном отдает предпочтение варианту 2 (например, ostream_iterator ищет оператор operator<<, a accumulate ищет оператор operator+ в пространстве имен вашего типа). В некоторых местах стандартная библиотека использует также вариант 3 (например, iterator_traits, char_traits) в основном потому, что эти классы свойств должны быть специализируемы для встроенных типов.

Заметим, что, к сожалению, стандартная библиотека C++ не всегда четко определяет точки настройки некоторых алгоритмов. Например, она ясно говорит о том, что трехпараметрическая версия accumulate должна вызывать пользовательский оператор operator+ с использованием второго варианта. Однако она не говорит, должен ли алгоритм sort вызывать пользовательскую функцию swap (обеспечивая таким образом преднамеренную точку настройки с использованием варианта 2), может ли он использовать пользовательскую функцию swap, и вызывает ли он функцию swap вообще; на сегодняшний день некоторые реализации sort используют пользовательскую функцию swap, в то время как другие реализации этого не делают. Важность рассматриваемой рекомендации была осознана совсем недавно, и сейчас комитет по стандартизации исправляет ситуацию, устраняя такие нечеткости из стандарта. Не повторяйте такие ошибки. (См. также рекомендацию 66.)



 
 

Библиотека программиста. 2009.
Администратор: admin@programmer-lib.ru