Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ
Шрифт:
Эмулировать такие преобразования с помощью определяемых пользователем «интеллектуальных» указателей не просто. Для этого нужно, чтобы компилировался такой код:
Разные конкретизации одного шаблона не связаны каким-либо отношением, поэтому компилятор считает, что SmartPtr<Middle> и SmartPtr<Top> – совершенно разные классы, не более связанные друг с другом, чем, например, vector<float> и Widget. Чтобы можно было осуществлять преобразования между разными классами SmartPtr, необходимо явно написать соответствующий код. В приведенном выше примере каждое предложение создает новый объект интеллектуального указателя, поэтому для начала сосредоточимся на написании конструкторов, которые будут вести себя так, как нам нужно. Ключевое наблюдение состоит в том, что невозможно написать сразу все необходимые конструкторы. В приведенной иерархии мы можем сконструировать SmartPtr<Top> из SmartPtr<Middle> или SmartPtr<Bottom>, но если в будущем иерархия будет расширена, то придется добавить возможность конструирования объектов SmartPtr<Top> из других типов интеллектуальных указателей. Например, если мы позже добавим такой класс:
то нужно будет поддержать создание объектов SmartPtr<Top> из SmartPtr<Below-Bottom>, и, очевидно, не хотелось бы ради этого модифицировать шаблон SmartPtr.
В принципе, нам может понадобиться неограниченное число конструкторов. Поскольку шаблон может быть конкретизирован для генерации неограниченного числа функций, похоже, что нам нужен не конструктор-функция для SmartPtr, а конструктор-шаблон. Это пример шаблона функции-члена (часто называемого шаблонного члена), то есть шаблона, генерирующего функции-члены класса:
Здесь говорится, что для каждой пары типов T и U класс SmartPtr<T> может быть создан из SmartPtr<U>, потому что SmartPtr<T> имеет конструктор, принимающий параметр типа SmartPtr<U>. Подобные конструкторы, создающие один объект из другого, тип которого является другой конкретизацией того же шаблона (например, SmartPtr<T> из SmartPtr<U>), иногда называют обобщенными конструкторами копирования.
Обобщенный конструктор копирования в приведенном выше примере не объявлен с модификатором explicit. И это сделано намеренно.
Будучи объявлен описанным выше образом, обобщенный конструктор копирования для SmartPtr предоставляет больше, чем нам нужно. Да, мы хотим иметь возможность создавать SmartPtr<Top> из SmartPtr<Bottom>, но вовсе не просили создавать SmartPtr<Bottom> из SmartPtr<Top>, потому что это противоречит смыслу открытого наследования (см. правило 32). Мы также не хотим создавать SmartPtr<int> из SmartPtr<double>, потому что не существует неявного преобразования int* в double*. Каким-то образом мы должны сузить многообразие функций-членов, которые способен генерировать этот шаблон.
Предполагая, что SmartPtr написан по образцу auto_ptr и tr1::shared_ptr, то есть предоставляет функцию-член get, которая возвращает копию встроенного указателя, хранящегося в объекте «интеллектуального» указателя (см. правило 15), мы можем воспользоваться реализацией шаблонного конструктора, чтобы ограничить набор преобразований:
Мы используем список инициализации членов, чтобы инициализировать член данных SmartPtr<T> типа T* указателем типа U*, который хранится в Smart-Ptr<U>. Этот код откомпилируется только тогда, когда существует неявное преобразование указателя U* в T*, а это как раз то, что нам нужно. Итак, SmartPtr<T> теперь имеет обобщенный копирующий конструктор, который компилируется только тогда, когда ему передается параметр совместимого типа.
Использование шаблонных функций-членов не ограничивается конструкторами. Еще одно полезное применение таких функций – поддержка присваивания. Например, класс shared_ptr из TR1 (см. правило 13) поддерживает конструирование из всех совместимых встроенных указателей, tr1::shared_ptr, auto_ptr и tr1::weak_ptr (см. правило 54), а также наличие в правой части оператора присваивания объекта любого из этих типов, кроме tr1::weak_ptr. Ниже приведен фрагмент спецификации TR1 для tr1::shared_ptr; обратите внимание, что при объявлении параметров шаблона используется ключевое слов class, а не typename. Как объясняется в правиле 42, в данном контексте они означают одно и то же.