Онлайн библиотека PLAM.RU


  • Правило 41: Разберитесь в том, что такое неявные интерфейсы и полиморфизм на этапе компиляции
  • Правило 42: Усвойте оба значения ключевого слова typename
  • Правило 43: Необходимо знать, как обращаться к именам в шаблонных базовых классах
  • Правило 44: Размещайте независимый от параметров код вне шаблонов
  • Правило 45: Разрабатывайте шаблоны функций-членов так, чтобы они принимали «все совместимые типы»
  • Правило 46: Определяйте внутри шаблонов функции, не являющиеся членами, когда желательны преобразования типа
  • Правило 47: Используйте классы-характеристики для предоставления информации о типах
  • Правило 48: Изучите метапрограммирование шаблонов
  • Глава 7

    Шаблоны и обобщенное программирование

    Изначально шаблоны в C++ появились для того, чтобы можно было реализовать безопасные относительно типов контейнеры: vector, list, map и им подобные. Однако по мере обретения опыта работы с шаблонами стали обнаруживаться все новые и новые способы их применения. Контейнеры были хороши сами по себе, но обобщенное программирование – возможность писать код, не зависящий от типа объектов, которыми он манипулирует, – оказалось еще лучше. Примерами такого программирования являются алгоритмы STL, такие как for_each, find и merge. В конечном итоге выяснилось, что механизм шаблонов C++ сам по себе является машиной Тьюринга: он может быть использован для вычисления любых вычисляемых значений. Это привело к метапрограммированию шаблонов: созданию программ, которые исполняются внутри компилятора C++ и завершают свою работу вместе с окончанием компиляции. В наши дни контейнеры – это лишь малая толика того, на что способны шаблоны C++. Но, несмотря на огромное разнообразие применений, в основе программирования шаблонов лежит небольшое число базовых идей. Именно им и посвящена настоящая глава.

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

    Правило 41: Разберитесь в том, что такое неявные интерфейсы и полиморфизм на этапе компиляции

    В мире объектно-ориентированного программирования преобладают явные интерфейсы и полиморфизм на этапе исполнения. Например, рассмотрим следующий (бессмысленный) класс:


    class Widget {

    public:

    Widget();

    virtual ~Widget();

    virtual std::size_t size() const;

    virtual void normalize();

    void swap(Widget& other); // см. правило 25

    ...

    };


    и столь же бессмысленную функцию:


    void doProcessing(Widget& w)

    {

    if(w.size() > 10 && w != someNastyWidget) {

    Widget temp(w);

    temp.normalize();

    temp.swap(w);

    }

    }


    Вот что мы можем сказать о переменной w в функции doProcessing:

    • Поскольку объявлено, что переменная w имеет тип Widget, то w должна поддерживать интерфейс Widget. Мы можем найти точное описание этого интерфейса в исходном коде (например, в заголовочном файле для Widget), поэтому я называю его явным интерфейсом – явно присутствующим в исходном коде программы.

    • Поскольку некоторые из функций-членов Widget являются виртуальными, то вызовы этих функций посредством w являются примером полиморфизма времени исполнения: конкретная функция, которую нужно вызвать, определяется во время исполнения на основании динамического типа w (см. правило 37).

    Мир шаблонного и обобщенного программирования принципиально отличается. В этом мире явные интерфейсы и полиморфизм времени исполнения продолжают существовать, но они менее важны. Вместо них на передний план выходят неявные интерфейсы и полиморфизм времени компиляции. Чтобы понять, что это означает, посмотрите, что произойдет, если мы превратим функцию doProcessing в шаблон функции:


    template<typename T>

    void doProcessing(T& w)

    {

    if(w.size() > 10 && w != someNastyWidget) {

    T temp(w);

    temp.normalize();

    temp.swap(w);

    }

    }


    Что теперь можно сказать о переменной w в шаблоне doProcessing?

    • Теперь интерфейс, который должна поддерживать переменная w, определяется операциями, выполняемыми над w в шаблоне. В данном случае видно, что тип переменной w (а именно T) должен поддерживать функции-члены size, normalize и swap; конструктор копирования (для создания temp), а также операцию сравнения на равенство (для сравнения с someNastyWidget). Скоро мы увидим, что это не совсем точно, но на данный момент достаточно. Важно, что набор выражений, которые должны быть корректны для того, чтобы шаблон компилировался, представляет собой неявный интерфейс, который тип T должен поддерживать.

    • Для успешного вызова функций, в которых участвует w, таких как operator> и operator!=, может потребоваться конкретизировать шаблон. Такая конкретизация происходит во время компиляции. Поскольку конкретизация шаблонов функций с разными шаблонными параметрами приводит к вызову разных функций, мы называем это полиморфизмом времени компиляции.

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

    Явные интерфейсы обычно состоят из сигнатур функций, то есть имен функций, типов параметров, возвращаемого значения и т. д. Так, открытый интерфейс класса Widget


    class Widget {

    public:

    Widget();

    virtual ~Widget();

    virtual std::size_t size() const;

    virtual void normalize();

    void swap(Widget& other);

    };


    состоит из конструктора, деструктора и функций size, normalize и swap вместе с типами их параметров, возвращаемых значений и признаков константности (интерфейс также включает генерируемые компилятором конструктор копирования и оператор присваивания – см. правило 5). В состав интерфейса могут входить также typedefbi.

    Неявный интерфейс несколько отличается. Он не базируется на сигнатурах функций. Вместо этого он состоит из корректных выражений. Посмотрим еще раз на условия в начале шаблона doProcessing:


    template<typename T>

    void doProcessing(T& w)

    {

    if(w.size() > 10 && w != someNastyWidget) {

    ...


    Неявному интерфейсу T (типа переменной w) присущи следующие ограничения:

    • Он должен предоставлять функцию-член по имени size, которая возвращает целое значение.

    • Он должен поддерживать функцию operator!=, которая сравнивает два объекта типа T. (Здесь мы предполагаем, что someNastyWidget имеет тип T.)

    Благодаря возможности перегрузки операторов ни одно из этих требований не должно удовлетворяться в обязательном порядке. Да, T должен поддерживать функцию-член size, хотя стоит упомянуть, что эта функция может быть унаследована от базового класса. Но эта функция не обязана возвращать целочисленный тип. Она даже может вообще не возвращать числовой тип. Вообще-то она даже не обязана возвращать тип, для которого определен operator>! Нужно лишь, чтобы она возвращала объект такого типа X, что может быть вызван operator>, которому передаются параметры типа X и int (потому что 10 имеет тип int). При этом функция operator> может и не принимать параметра, тип которого в точности совпадает с X; достаточно, если тип ее параметра Y может быть неявно преобразован к типу X!

    Аналогично не требуется, чтобы тип T поддерживал operator!=, достаточно будет и того, чтобы функция operator!= принимала один объект типа X и один объект типа Y. Если T можно преобразовать в X, а someNastyWidget в Y, то вызов operator!= будет корректным.

    (Кстати говоря: мы не принимаем во внимание возможность перегрузки operator&&, в результате которой семантика приведенного выражения может стать уже не конъюнкцией, а чем-то совершенно иным.)

    У большинства людей голова идет кругом, когда они начинают задумываться о неявных интерфейсах, но на самом деле ничего страшного в них нет. Неявные интерфейсы – это просто набор корректных выражений. Сами по себе выражения могут показаться сложными, но налагаемые ими ограничения достаточно очевидны.


    if(w.size() > 10 && w != someNastyWidget)...


    Мало что можно сказать об ограничениях, налагаемых функциями size, operator>, operator&& или operator!=, но идентифицировать ограничения всего выражения в целом легко. Условная часть предложения if должна быть булевским выражением, поэтому независимо от конкретных типов результат вычисления (w.size() > 10 && w!= someNastyWidget) должен быть совместим с bool. Это та часть неявного интерфейса, которую шаблон doProcessing налагает на свой параметр типа T. Кроме того, для работы doProcessing необходимо, чтобы интерфейс типа T допускал обращения к конструктору копирования, а также функциям normalize, size и swap.

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

    Что следует помнить

    • И классы, и шаблоны поддерживают интерфейсы и полиморфизм.

    • Для классов интерфейсы определены явно и включают главным образом сигнатуры функций. Полиморфизм проявляется во время исполнения – через виртуальные функции.

    • Для параметров шаблонов интерфейсы неявны и основаны на корректных выражениях. Полиморфизм проявляется во время компиляции – через конкретизацию и разрешение перегрузки функций.

    Правило 42: Усвойте оба значения ключевого слова typename

    Вопрос: какая разница между «class» и «typename» в следующем объявлении шаблона:


    template <class T> class Widget; // использует “class”

    template <typename T> class Widget; // использует “typename”


    Ответ: никакой. Когда в шаблоне объявляется параметр типа, class и type-name означают абсолютно одно и то же. Некоторые программисты предпочитают всегда писать class, потому что это слово короче. Другие (включая меня) предпочитают typename, поскольку оно говорит о том, что параметром не обязательно должен быть тип класса. Некоторые разработчики используют typename, когда допускается любой тип, и резервируют слово class для случаев, когда допускается только тип, определяемый пользователем. Но с точки зрения C++, class и typename в объявлении параметра шаблона означают в точности одно и то же.

    Однако не всегда в C++ ключевые слова class и typename эквивалентны. Иногда вы обязаны использовать typename. Чтобы понять – когда именно, поговорим о двух типах имен, на которые можно ссылаться в шаблоне.

    Предположим, что у нас есть шаблон функции, принимающей в качестве параметра совместимый с STL-контейнер, содержащий объекты, которые могут быть присвоены величинам типа int. Далее предположим, что эта функция просто печатает значение второго элемента. Это не очень содержательная функция, которая к тому же и реализована по-дурацки. Как я уже говорил, она даже не будет компилироваться, но забудьте об этом на время – все это не так глупо, как кажется:


    template <typename C> // печатает второй

    void print2nd(const C& container) // элемент контейнера

    { // это некорректный C++!

    if (container.size() >= 2) {

    C::const_iterator iter(container.begin()); // получить итератор,

    // указывающий на первый

    // элемент

    ++iter; // сместиться на второй

    // элемент

    int value = *iter; // скопировать элемент в int

    std::cout << value; // напечатать int

    }

    }


    Я выделил в этой функции две локальные переменные – iter и value. Типом iter является C::const_iterator – он зависит от параметра шаблона C. Имена в шаблоне, которые зависят от параметра шаблона, называются зависимыми именами. Зависимое имя внутри класса я буду называть вложенным зависимым именем. C::const_iterator – это вложенное зависимое имя. Фактически это даже вложенное зависимое имя типа, то есть вложенное имя, которое относится к типу.

    Другая локальная переменная в print2nd – value – имеет тип int, а int – это имя, которое не зависит ни от какого параметра шаблона. Такие имена называются независимыми.

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


    template <typename C> // печатает второй элемент контейнера

    void print2nd(const C& container) // это некорректный C++!

    {

    C::const_iterator *x;

    ...

    }


    Выглядит так, будто мы объявили x как локальную переменную – указатель на C::const_iterator. Но это только видимость, поскольку мы «знаем», что C::const_iterator является типом. А что, если в классе C есть статический член данных по имени const_iterator и что, если x будет именем глобальной переменной? В этом случае приведенный код не будет объявлять локальную переменную, а окажется умножением C::const_iterator на x! Звучит невероятно, но это возможно, и авторы синтаксических анализаторов исходного кода на C++ должны позаботиться обо всех возможных вариантах входных данных, даже самых сумасшедших.

    Пока о C ничего не известно, мы не можем узнать, является ли C::const_iterator типом или нет, а во время разбора шаблона print2nd компилятор ничего о C не знает. В C++ предусмотрено правило, разрешающее эту неопределенность: если синтаксический анализатор встречает вложенное зависимое имя в шаблоне, он предполагает, что это не имя типа, если только вы не укажете это явно. По умолчанию вложенные зависимые имена не являются типами. Есть исключение из этого правила, о котором я расскажу чуть ниже.

    Имея это в виду, посмотрите опять на начало print2nd:


    template <typename C>

    void print2nd(const C& container)

    {

    if (container.size() >= 2) {

    C::const_iterator iter(container.begin()); // предполагается, что

    ... // это не имя типа


    Теперь должно быть ясно, почему это некорректный C++. Объявление iter имеет смысл только в случае, если C::const_iterator является типом, но мы не сообщили C++ об этом, потому C++ предполагает, что это не так. Чтобы исправить ситуацию, мы должны сообщить C++, что C::const_iterator – это тип. Для этого мы помещаем ключевое слово typename непосредственно перед ним:


    template <typename C> // это корректный С++

    void print2nd(const C& container)

    {

    if (container.size() >= 2) {

    typename C::const_iterator iter(container.begin());

    ...

    }

    }


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

    Слово typename следует использовать для идентификации только вложенных зависимых имен типов; для других имен оно не применяется. Вот пример шаблона функции, который принимает и контейнер, и итератор для этого контейнера:


    template <typename C> // допускается typename (как и “class”)

    void f(const C& container, // typename не допускается

    typename C::iterator iter); // typename требуется


    C не является вложенным зависимым именем типа (оно не вложено внутрь чего-либо, зависимого от параметра шаблона), поэтому его не нужно предварять словом typename при объявлении контейнера, но C::iterator – это вложенное зависимое имя типа, поэтому перед ним следует поставить typename.

    Из правила «typename должно предварять вложенные зависимые имена типов» есть исключение: typename не должно предварять вложенные зависимые имена типов в списке базовых классов или в идентификаторе базового класса в списке инициализации членов. Например:


    template <typename T>

    class Derived: public Base<T>::Nested { // список базовых классов:

    public: // typename не допускается

    explicit Derived(int x)

    :Base<T>::Nested(x) // идентификатор базового класса

    { // в списке инициализации членов:

    // typename не допускается

    typename Base<T>::Nested temp; // использование вложенного

    ... // зависимого имени типа не как

    } // идентификатора базового

    ... // класса в списке инициализации

    }; // членов: typename необходимо


    Такая несогласованность несколько раздражает, но по мере приобретения опыта вы перестанете ее замечать.

    Рассмотрим еще один пример использования typename, потому нечто подобное можно встретить в реальном коде. Предположим, что мы пишем шаблон функции, которая принимает итератор, и хотим сделать локальную копию – temp – объекта, на который этот итератор указывает. Это можно сделать примерно так:


    template <typename IterT>

    void workWithIterator(IterT iter)

    {

    typename std::iterator_traits<IterT>::value_type temp(*iter);

    ...

    }


    Не пугайтесь при виде выражения std::iterator_traits<IterT>::value_type. Здесь просто используются стандартные классы-характеристики (traits) (см. правило 47). Так, на C++ говорят «тип того, на что указывает объект типа *IterT». В этом предложении объявлена локальная переменная (temp) того же типа, что и объекты, на которые указывает IterT, а затем она инициализирована значением, на которое указывает iter. Если IterT будет типа vector<int>::iterator, то temp будет иметь тип int. Если же IterT будет типа vector<string>::iterator, то temp будет иметь тип string. Поскольку std::iterator_traits<IterT>::value_type – это вложенное зависимое имя типа (value_type вложено внутрь iterator_traits<IterT>, а IterT – параметр шаблона), мы должны предварить его словом typename.

    Если вам неприятно даже видеть выражение std::iterator_traits<IterT>::value_type, представьте, каково набирать его на клавиатуре. Если вы, как и большинство программистов, считаете, что набрать такое более одного раза немыслимо, определите псевдоним для этого типа посредством typedef. Для имен членов классов-характеристик, к каковым относится value_type, (см. в правиле 47 информацию о классах-характеристиках), принято соглашение, согласно которому имя typedef должно совпадать с именем члена. Таким образом, определение локального typedef обычно выглядит так:


    template <typename IterT>

    void workWithIterator(IterT iter)

    {

    typedef typename std::iterator_traits<IterT>::value_type value_type;

    value_type temp(*iter);

    ...

    }


    Многих программистов соседство typedef и typename поначалу раздражает, но это логическое следствие из правила обращения к вложенным зависимым именам типов. Вы скоро привыкнете. К тому же у вас есть на то веские причины. Сколько раз вы готовы напечатать std::iterator_traits<IterT>::value_type?

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

    Что следует помнить

    • В объявлениях параметров шаблона ключевые слова class и typename взаимозаменяемы.

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

    Правило 43: Необходимо знать, как обращаться к именам в шаблонных базовых классах

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


    class CompanyA {

    public:

    ...

    void sendClearText(const std::string& msg);

    void sendEncryptedText(const std::string& msg);

    ...

    };

    class CompanyB{

    public:

    ...

    void sendClearText(const std::string& msg);

    void sendEncryptedText(const std::string& msg);

    ...

    };

    ... // классы для других компаний

    class MsgInfo {...}; // класс, содержащий информацию,

    // используемую для создания

    // сообщения

    template<typename Company>

    class MsgSender {

    public:

    ... // конструктор, деструктор и т. п.

    void sendClear(const MsgInfo& info)

    {

    std::string msg;

    создать msg из info

    Company c;

    c.sendClearText(msg);

    }

    void sendSecret(const MsgInfo& info) // аналогично sendClear, но вызывает

    {...} // c.sendEncrypted

    };


    Эта программа будет работать. Но предположим, что иногда мы хотим протоколировать некоторую информацию при отправке сообщений. Такую возможность легко добавить, написав производный класс, и, на первый взгляд, разумно это сделать следующим образом:


    template <typename Company>

    class LoggingMsgSender: public MsgSender<Company> {

    public:

    ...

    void sendClearMsg(const MsgInfo& info)

    {

    записать в протокол перед отправкой;

    sendClear(info); // вызвать функцию из базового класса

    // этот код не будет компилироваться!

    записать в протокол после отправки;

    }

    ...

    };


    Отметим, что функция, отправляющая сообщение, в производном классе называется иначе (sendClearMsg), чем в базовом (sendClear). Это хорошее решение, потому что таким образом мы обходим проблему сокрытия унаследованных имен (см. правило 33), а равно сложности, возникающие при переопределении наследуемых невиртуальных функций (см. правило 36). Но этот код не будет компилироваться, по крайней мере, компилятором, совместимым со стандартом. Такой компилятор решит, что функции sendClear не существует. Мы видим, что эта функция определена в базовом классе, но компилятор не станет искать ее там. Попытаемся понять – почему.

    Проблема в том, что когда компилятор встречает определение шаблона класса LoggingMsgSender, он не знает, какому классу тот наследует. Понятно, что классу MsgSender<Company>, но Company – параметр шаблона, который не известен до момента конкретизации LoggingMsgSender. Не зная, что такое Company, невозможно понять, как выглядит класс MsgSender<Company>. В частности, не существует способа узнать, есть ли в нем функция sendClear.

    Чтобы яснее почувствовать, в чем сложность, предположим, что у нас есть класс CompanyZ, описывающий компанию, которая настаивает на том, чтобы все сообщения шифровались:


    class CompanyZ { // этот класс не представляет

    public: // функции sendCleartext

    ...

    void sendEncrypted(const std::string& msg);

    ...

    };


    Общий шаблон MsgSender не подходит для CompanyZ, потому что в нем определена функция sendClear, которая для объектов класса CompanyZ не имеет смысла. Чтобы решить эту проблему, мы можем создать специализированную версию MsgSender для CompanyZ:


    template <> // полная специализация MsgSender;

    class MsgSender <CompanyZ> { // отличается от общего шаблона

    public: // только отсутствием функции

    ... // sendCleartext

    void sendSecret(const MsgInfo& info)

    {...}

    };


    Обратите внимание на синтаксическую конструкцию «template<>» в начале определения класса. Она означает, что это и не шаблон, и не автономный класс. Это специализированная версия шаблона MsgSender, которая должна использоваться, если параметром шаблона является CompanyZ. Называется это полной специализацией шаблона : шаблон MsgSender специализирован для типа CompanyZ, и эта специализация применяется, коль скоро в качестве параметра указан тип CompanyZ, никакие другие особенности параметров шаблона во внимание не принимаются.

    Имея специализацию шаблона MsgSender для CompanyZ, снова рассмотрим производный класс LoggingMsgSender:


    template <typename Company>

    class LoggingMsgSender: public MsgSender<Company> {

    public:

    ...

    void sendClearMsg(const MsgInfo& info)

    {

    записать в протокол перед отправкой;

    sendClear(info); // если Company == CompanyZ,

    // то этой функции не существует

    записать в протокол после отправки;

    }

    ...

    };


    Как следует из комментария, этот код просто не имеет смысла, если базовым классом является MsgSender<CompanyZ>, так как в нем нет функции sendClear. Поэтому C++ отвергнет такой вызов; компилятор понимает, что шаблон базового класса можно специализировать, и интерфейс, предоставляемый этой специализацией, может быть не таким, как в общем шаблоне. В результате компилятор обычно не ищет унаследованные имена в шаблонных базовых классах. В некотором смысле, когда мы переходим от «объектно-ориентированного C++» к «C++ с шаблонами» (см. правило 1), наследование перестает работать.

    Чтобы исправить ситуацию, нужно как-то заставить C++ отказаться от догмы «не заглядывай в шаблонные базовые классы». Добиться этого можно тремя способами. Во-первых, можно предварить обращения к функциям из базового класса указателем this:


    template <typename Company>

    class LoggingMsgSender: public MsgSender<Company> {

    public:

    ...

    void sendClearMsg(const MsgInfo& info)

    {

    записать в протокол перед отправкой

    ;

    this->sendClear(info); // порядок! Предполагается, что

    // sendClear будет унаследована

    записать в протокол после отправки

    ;

    }

    ...

    };


    Во-вторых, можно воспользоваться using-объявлением. Мы уже обсуждали эту тему в правиле 33, где было сказано, что using-объявлением делает скрытые имена из базового класса видимыми в производном классе. Поэтому мы можем переписать sendClearMsg следующим образом:


    template <typename Company>

    class LoggingMsgSender: public MsgSender<Company> {

    public:

    using MsgSender<Company>::sendClear; // сообщает компилятору о том, что

    ... // sendClear есть в базовом классе

    void sendClearMsg(const MsgInfo& info)

    {

    ...

    sendClear(info); // нормально, предполагается, что

    ... // sendClear будет унаследована

    }

    ...

    };


    Хотя using-объявление будет работать как здесь, так и в правиле 33, но используются они для решения разных задач. Здесь проблема не в том, что имена из базового класса скрыты за именами, объявленными в производном классе, а в том, что компилятор вообще не станет производить поиск в области видимости базового класса, если только вы явно не попросите его об этом.

    И последний способ заставить ваш код компилироваться – явно указать, что вызываемая функция находится в базовом классе:


    template <typename Company>

    class LoggingMsgSender: public MsgSender<Company> {

    pubilc:

    ...

    void sendClearMsg(const MsgInfo& info)

    {

    ...

    MsgSender<Company>::sendClear(info); // нормально, предполагается, что

    ... // sendClear будет унаследована

    }

    ...

    };


    Но этот способ хуже прочих, посколько если вызываемая функция виртуальна, то явная квалификация отключает динамическое связывание.

    С точки зрения видимости имен, все три подхода эквивалентны: они обещают компилятору, что любая специализация шаблона базового класса будет поддерживать интерфейс, предоставленный общим шаблоном. Такое обещание – это все, что необходимо компилятору во время синтаксического анализа производного шаблонного класса, подобного LoggingMsgSender, но если данное обещание не будет выполнено, истина всплывет позже. Например, если в программе есть такой код:


    LoggingMsgSender<CompanyZ> zMsgSender;

    MsgInfo msgData;

    ... // поместить info в msgData

    zMsgSender.sendClearMsg(msgData); // ошибка! не скомпилируется


    то вызов sendClearMsg не скомпилируется, потому что в этой точке компилятор знает, что базовый класс – это специализация шаблона MsgSender<CompanyZ> и в нем нет функции sendClear, которую sendClearMsg пытается вызвать.

    Таким образом, суть дела в том, когда компилятор диагностирует неправильные обращения к членам базового класса – раньше (когда анализируются определения шаблонов производного класса) или позже (когда эти шаблоны конкретизируются переданными в шаблон аргументами). C++ предпочитает раннюю диагностику, и поэтому предполагает, что о содержимом базовых классов, конкретизируемых из шаблонов, не известно ничего.

    Что следует помнить

    • В шаблонах производных классов ссылки на имена из шаблонов базовых классов осуществляются с помощью префикса «this->», using-объявления либо посредством явного указания базового класса.

    Правило 44: Размещайте независимый от параметров код вне шаблонов

    Шаблоны – чудесный способ сэкономить время и избежать дублирования кода. Вместо того чтобы вводить код 20 похожих классов, в каждом из которых по 15 функций-членов, вы набираете текст одного шаблона и поручаете компилятору сгенерировать 20 конкретных классов и все 300 необходимых вам функций. (Функции-члены шаблонов классов неявно генерируются, только когда программа к ним обращается, поэтому все 300 функций-членов вы получите, лишь если будете все их использовать.) Шаблоны функций не менее привлекательны. Вместо написания множества однотипных функций вы пишете один шаблон и позволяете компиляторам проделать все остальное. Ну разве не восхитительная технология?

    Да… иногда. Если вы не будете внимательны, то использование шаблонов может привести к разбуханию кода. Так называется дублирование в двоичной программе кода, данных или того и другого. В результате компактный и лаконичный исходный код в объектном виде становится громоздким и тяжелым. Хорошего в этом мало, поэтому нужно знать, как избежать такой неприятности.

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

    Когда вы пишете функцию и обнаруживаете, что некоторая часть ее реализации мало чем отличается от реализации другой функции, разве вы дублируете код? Конечно, нет. Вы исключаете общую часть из обеих функций, помещаете ее в третью, а первые две вызывают эту третью функцию. Иными словами, вы анализируете эти две функции на предмет выявления общих и отличающихся частей, перемещаете общие части в новую функцию, а отличающиеся части оставляете на месте. Аналогично, если вы пишете класс и выясняется, что некоторые части этого класса в точности совпадают с частями другого класса, вы не станете их дублировать, а просто вынесете общие части в новый класс, а затем воспользуетесь наследованием или композицией (см. правила 32, 38 и 39), предоставив исходному классу доступ к общим средствам. Отличающиеся части исходных классов остаются на месте.

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

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


    template<typename T, std::size_t n> // шаблон матрицы размерностью n x n,

    class SquareMatrix { // состоящей из объектов типа T;

    public: // см. ниже информацию о параметре size_t

    ...

    void invert(); // обращение матрицы на месте

    };


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

    Теперь рассмотрим такой код:


    SquareMatrix<double, 5> sm1;

    ...

    sm1.invert(); // вызов SquareMatrix<double, 5>::invert()

    SquareMatrix<double, 10> sm2;

    ...

    sm2.invert(); // вызов SquareMatrix<double, 10>::invert()


    Здесь будут конкретизированы две копии функции invert. Они не идентичны, потому что одна из них работает с матрицами 5x5, а другая – с матрицами 10x10, но во всем остальном, кроме констант 5 и 10, эти функции ничем не отличаются. Это – классический пример разбухания кода в результате применения шаблонов.

    Что вы делаете, когда есть две функции, абсолютно одинаковые, за исключением того, что в одной используется константа 5, а в другой – 10? Естественно, вы создаете функцию, которая принимает параметр, а затем вызываете ее, один раз передавая в качестве параметра 5, а другой раз – 10. Вот первая попытка проделать тот же трюк в реализации шаблона SquareMatrix:


    template<typename T> // базовый класс, не зависящий

    class SquareMatrixBase { // от размерности матрицы

    protected:

    ...

    void invert(std::size_t matrixSize); // обратить матрицу заданной

    ... // размерности

    };

    template<typename T, std::size_t n>

    class SquareMatrix: private SquareMatrixBase<T> {

    private:

    using SquareMatrixBase<T>::invert; // чтобы избежать сокрытия базовой

    // версии invert; см. правило 33

    public:

    ...

    void invert() {this->invert(n);} // встроенный вызов версии invert

    }; // из базового класса

    // см. ниже – почему

    // применяется “this->”


    Как видите, параметризованная версия функции invert находится в базовом классе – SquareMatrixBase. Как и SquareMatrix, SquareMatrixBase – шаблон, но в отличие от SquareMatrix, он имеет только один параметр – тип объектов в матрице, но не имеет параметра size. Поэтому все матрицы, содержащие объекты заданного типа, будут разделять общий класс SquareMatrixBase. И, значит, все они разделят единственную копию функции invert из данного класса.

    Назначение SquareMatrixBase::invert – помочь избежать дублирования кода в производных классах, поэтому using-объявление помещено в секцию protected, а не public. Дополнительные расходы на вызов этой функции нулевые, поскольку в производных классах ее вызовы invert встроены (встраивание неявное – см. правило 30). Во встроенных функциях применяется нотация «this->», потому что в противном случае, как следует из правила 43, имена функций из шаблонного базового класса (SquareMatrixBase<T>) будут скрыты от подклассов. Отметим также, что наследование SquareMatrix от SquareMatrixBase – закрытое. Это отражает тот факт, что базовый класс введен только для одной цели – упростить реализацию производных, и не означает наличия концептуального отношения «является» между SquareMatrixBase и SquareMatrix (о закрытом наследовании см. правило 39).

    До сих пор все шло хорошо, но имеется одна проблема, которую нам еще предстоит решить. Откуда класс SquareMatrixBase узнает, с какими данными он должен работать? Размерность матрицы ему известна из параметра, но как узнать, где находятся сами данные конкретной матрицы? По-видимому, это известно только производному классу. А как производный класс может передать эту информацию базовому, чтобы тот мог выполнить обращение матрицы?

    Один из возможных способов – добавить дополнительный параметр в функцию SquareMatrixBase::invert, скажем, указатель на начало участка памяти, где размещаются данные матрицы. Это будет работать, но, скорее всего, invert – не единственная функция в классе SquareMatrix, которая может быть написана так, что не будет зависеть от размерности, и перенесена в класс SquareMatrixBase. Если таких функций будет несколько, всем им понадобится знать, где находятся данные матрицы. Нам придется в каждую добавлять новый параметр, и получится, что мы многократно передаем SquareMatrixBase одну и ту же информацию. Как-то неправильно это.

    Есть альтернатива – хранить указатель на данные матрицы в SquareMatrixBase. И там же можно хранить размерность матрицы. Получается такой код:


    template<typename T>

    class SquareMatrixBase {

    protected:

    SquareMatrixBase(std::size_t n, T pMem) // сохраняет размерность

    :size(n), pData(pMem){} // и указатель на данные матрицы

    void setData(T *ptr) { pData = ptr;} // присвоить значение pData

    ...

    private:

    std::size_t size; // размерность матрицы

    T *pData; // указатель на данные матрицы

    };


    Это позволяет производным классам решать, как выделять память. Возможна, в частности, реализация, при которой данные матрицы сохраняются прямо в объекте SquareMatrix:


    template<typename T, size_t size>

    class SquareMatrix: private SquareMatrixBase {

    public:

    SquareMatrix() // передать базовому классу размерность

    :SquareMatrixBase<t>(n, data) {} // матрицы и указатель на данные

    ...

    private:

    T data(n*n);

    };


    Объекты такого типа не нуждаются в динамическом выделении памяти, но зато могут быть очень большими. Вместо этого можно выделять память для данных матрицы из кучи:


    template<typename T, size_t size>

    class SquareMatrix: private SquareMatrixBase {

    public:

    SquareMatrix() // присвоить указателю на данные

    :SquareMatrixBase<t>(n, 0), // в базовом классе значение null

    pData(new T(n*n)) // выделить память для данных матрицы,

    {this->setDataPtr(pData.get();} // сохранить указатель на нее и передать

    ... // его копию базовому классу

    private:

    boost::scoped_array<T> pData; // о классе boost::scoped_array

    }; // см. правило 13


    Независимо от того, где хранятся данные, с точки зрения «разбухания» кода важно лишь, что теперь многие (быть может, все) функции-члены SquareMatrix оказываются просто встроенными вызовами их версий из базового класса, которые теперь будут разделяются всеми матрицами, содержащими данные одного и того же типа, независимо от их размера. В то же время объекты SquareMatrix разных размеров относятся к разным типам. Поэтому, несмотря на то что классы SquareMatrix<double, 5> и SquareMatrix<double, 10> пользуются одними и теми же функциями, определенными в SquareMatrixBase<double>, не получится передать функции, ожидающей параметра типа SquareMatrix<double, 10>, объект типа SquareMatrix<double, 5>. Хорошо, не правда ли?

    Да, хорошо, но не бесплатно. Для функции invert с жестко «зашитой» в исходный текст размерностью матрицы, скорее всего, был бы сгенерирован более эффективный код, чем разделяемой функции, которой размерность передается в качестве параметра либо хранится в самом объекте. Например, «зашитая» размерность может быть константой времени компиляции, так что к ней будут применимы различные виды оптимизации, в частности встраивание константы непосредственно в машинную команду в виде непосредственного операнда. Для функции, не зависящей от размерности, такой номер не пройдет.

    С другой стороны, наличие только одной версии invert для разных размерностей уменьшает объем исполняемого кода, а это, в свою очередь, уменьшит размер рабочего множества программы и улучшит локальность ссылок в кэше команд. Это может ускорить исполнение программы настолько, что все потери эффективности по сравнению с зависящей от размерности версией будут с лихвой компенсированы. Какой эффект окажется доминирующим? Единственный способ получить ответ – попробовать оба варианта и исследовать поведение на вашей конкретной платформе с репрезентативными наборами данных.

    Другой фактор, влияющий на эффективность, – это размеры объектов. Если вы не будете внимательны, то перенос независимых от размерности функций в базовый класс может привести к увеличению размера каждого объекта. Например, в только что приведенном коде для каждого объекта SquareMatrix имеется указатель на его данные в классе SquareMatrixBase, несмотря даже на то, что производный класс и так может получить эти данные. Это увеличивает размер каждого объекта SquareMatrix, по крайней мере, на размер указателя. Можно модифицировать класс так, чтобы необходимость в этих указателях отпала, но это компромисс. Например, если завести в базовом классе защищенный член для хранения указателя на данные матрицы, то мы пожертвуем инкапсуляцией (см. правило 22). Это также может привести к усложнению алгоритмов управления ресурсами. Если в базовом классе хранится указатель на данные матрицы, то память для этих данных может быть либо выделена динамически, либо физически находиться внутри объекта производного класса (как мы видели). Так как же базовый класс определит, следует ли удалять указатель? Ответы на такие вопросы существуют, но чем изощреннее ваш дизайн, тем сложнее все получается. В некоторый момент умеренное дублирование кода может даже показаться спасением.

    В этом правиле мы обсуждаем только разбухание кода из-за параметров шаблонов, не являющихся типами, но и параметры-типы могут привести к тому же. Например, на многих платформах int и long имеют одно и то же двоичное представление, поэтому функции-члены, скажем, для vector<int> и vector<long>, могут оказаться идентичными – разбухание в чистом виде. Некоторые компоновщики объединяют идентичные реализации функций, а некоторые – нет, и, значит, некоторые шаблоны, конкретизированные для int и для long, на одних платформах приводят к разбуханию, а на других – нет. Аналогично на большинстве платформ все типы указателей имеют одинаковое двоичное представление, поэтому шаблоны с параметрами указательных типов (например, list<int*>, list<const int*>, list<SquareMatrix<long,3>*> и т. п.) зачастую могли бы использовать общие реализации всех функций-членов. Как правило, это означает, что функции-члены, которые работают со строго типизованными указателями (например, T*) должны внутри себя вызывать функции, работающие с нетипизированными указателями (то есть void*). В некоторых реализациях стандартной библиотеки C++ такой подход применен к шаблонам vector, deque и list и им подобным. Если вас беспокоит опасность разбухания кода из-за использования шаблонов, возможно, стоит поступить аналогично.

    Что следует помнить

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

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

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

    Правило 45: Разрабатывайте шаблоны функций-членов так, чтобы они принимали «все совместимые типы»

    Интеллектуальные указатели – это объекты, которые ведут себя во многом подобно обычным указателям, но добавляют функциональность, которую последние не предоставляют. Например, в правиле 13 объясняется, как можно использовать стандартные классы auto_ptr и tr1::shared_ptr для автоматического удаления динамически выделенных ресурсов в нужное время. Итераторы STL-контейнеров почти всегда являются интеллектуальными указателями. Понятно, что от обычного указателя нельзя ожидать, что он будет сдвигаться на следующий узел связанного списка в результате выполнения операции «++», но итератор списка list::iterator работает именно так.

    Для чего обычные указатели хороши – так это для поддержки неявных преобразований типов. Указатели на объекты производных классов неявно преобразуются в указатели на объекты базовых классов, указатели на неконстантные объекты – в указатели на константные и т. п. Например, рассмотрим некоторые преобразования, которые могут происходить в трехуровневой иерархии:


    class Top {...};

    class Middle: public Top {...};

    class Bottom: public Middle {...};

    Top *pt1 = new Middle; // преобразует Middle* в Top*

    Top *pt2 = new Bottom; // преобразует Middle* в Bottom*

    Const Top *pct2 = pt1; // преобразует Top* в const Top*


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


    Template<typename T>

    class SmartPtr {

    public:

    explicit SmartPtr(T *realPtr); // интеллектуальные указатели обычно

    ... // инициализируются встроенными

    }; // указателями

    SmartPtr<Top> pt1 = // преобразует SmartPtr<Middle>

    SmartPtr<Middle>(new Middle); // в SmartPtr<Top>

    SmartPtr<Top> pt2 = // преобразует SmartPtr<Bottom>

    SmartPtr<Bottom>(new Bottom); // SmartPtr<Top>

    SmartPtr<const Top> pct2 = pt1;


    Разные конкретизации одного шаблона не связаны каким-либо отношением, поэтому компилятор считает, что SmartPtr<Middle> и SmartPtr<Top> – совершенно разные классы, не более связанные друг с другом, чем, например, vector<float> и Widget. Чтобы можно было осуществлять преобразования между разными классами SmartPtr, необходимо явно написать соответствующий код. В приведенном выше примере каждое предложение создает новый объект интеллектуального указателя, поэтому для начала сосредоточимся на написании конструкторов, которые будут вести себя так, как нам нужно. Ключевое наблюдение состоит в том, что невозможно написать сразу все необходимые конструкторы. В приведенной иерархии мы можем сконструировать SmartPtr<Top> из SmartPtr<Middle> или SmartPtr<Bottom>, но если в будущем иерархия будет расширена, то придется добавить возможность конструирования объектов SmartPtr<Top> из других типов интеллектуальных указателей. Например, если мы позже добавим такой класс:


    class BelowBottom: public Bottom {...};


    то нужно будет поддержать создание объектов SmartPtr<Top> из SmartPtr<Below-Bottom>, и, очевидно, не хотелось бы ради этого модифицировать шаблон SmartPtr.

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


    template<typename T>

    class SmartPtr {

    public:

    template<typename U> // шаблонный член

    SmartPtr(const SmartPtr<U>& other); // для «обобщенного

    ... // конструктора копирования»

    };


    Здесь говорится, что для каждой пары типов T и U класс SmartPtr<T> может быть создан из SmartPtr<U>, потому что SmartPtr<T> имеет конструктор, принимающий параметр типа SmartPtr<U>. Подобные конструкторы, создающие один объект из другого, тип которого является другой конкретизацией того же шаблона (например, SmartPtr<T> из SmartPtr<U>), иногда называют обобщенными конструкторами копирования.

    Обобщенный конструктор копирования в приведенном выше примере не объявлен с модификатором explicit. И это сделано намеренно. Преобразования типов между встроенными типами указателей (например, из указателя на производный класс к указателю на базовый класс) происходят неявно и не требуют приведения, поэтому разумно и для интеллектуальных указателей эмулировать такое поведение. Именно поэтому и не указано слово 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), мы можем воспользоваться реализацией шаблонного конструктора, чтобы ограничить набор преобразований:


    template<typename T>

    class SmartPtr {

    public:

    template<typename U>

    SmartPtr(const SmartPtr<U>& other) // инициировать этот хранимый

    :heldPtr(other.get()) {...} // указатель указателем, хранящимся

    // в другом объекте

    T *get() const { return heldPtr;}

    ...

    private: // встроенный указатель,

    T *heldPtr; // хранящийся в «интеллектуальном»

    };


    Мы используем список инициализации членов, чтобы инициализировать член данных 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, в данном контексте они означают одно и то же.


    template<class T> class shared_ptr {

    public:

    template<class Y> // конструирует из

    explicit shared_ptr(Y *p); // любого совместимого

    template<class Y> // встроенного указателя,

    shared_ptr(shared_ptr<Y> const& r); // shared_ptr,

    template<class Y> // weak_ptr или

    explicit shared_ptr(weak_ptr<Y> const& r); // auto_ptr

    template<class Y>

    explicit shared_ptr(auto_ptr<Y>& r);

    template<class Y> // присваивает

    explicit shared_ptr& operator=(shared_ptr<Y> const& r); // любой

    template<class Y> // совместимый

    explicit shared_ptr& operator=(auto_ptr<Y> const& r); // shared_ptr или

    ... // auto_ptr

    };


    Все эти конструкторы объявлены как explicit, за исключением обобщенного конструктора копирования. Это значит, что неявные преобразования от одного типа shared_ptr к другому допускаются, но неявные преобразования от встроенного указателя или другого «интеллектуального» указателя не допускаются. (Явные преобразования – например, с помощью приведения типов – разрешены). Также интересно отметить, что при передаче объекта auto_ptr конструктору tr1::shared_ptr и оператору присваивания параметр указывается без модификатора const, тогда как передаваемые параметры типа tr1::shared_ptr и tr1::weak_ptr константны. Это следствие того факта, что в отличие от других классов объекты auto_ptr модифицируются при копировании (см. правило 13).

    Шаблонные функции-члены – чудесная вещь, но они не отменяют основных правил языка. В правиле 5 объясняется, что две из четырех функций-членов, которые компиляторы могут генерировать автоматически, – это конструктор копирования и оператор присваивания. В классе tr1::shared_ptr объявлен обобщенный конструктор копирования, и ясно, что в случае совпадения типов T и Y конкретизация обобщенного конструктора копирования может быть сведена к созданию «обычного» конструктора копирования. Поэтому возникает вопрос, что будет делать компилятор в случае, когда один объект tr1::shared_ptr конструируется из другого объекта того же типа: генерировать обычный конструктор копирования для tr1::shared_ptr или конкретизировать обобщенный конструктор копирования из шаблона?

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


    template<class T> class shared_ptr {

    public:

    shared_ptr(shared_ptr const& r); // конструктор копирования

    template<class Y> // обобщенный

    shared_ptr(shared_ptr<Y> const& r); // конструктор копирования

    shared_ptr& operator=(shared_ptr const& r); // оператор присваивания

    template<class Y> // обобщенный оператор

    shared_ptr& operator=(shared_ptr<Y> const& r); // присваивания

    ...

    };


    Что следует помнить

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

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

    Правило 46: Определяйте внутри шаблонов функции, не являющиеся членами, когда желательны преобразования типа

    В правиле 24 объясняется, почему только к свободным функциям применяются неявные преобразования типов всех аргументов. В качестве примера была приведена функция operator* для класса Rational. Прежде чем продолжить чтение, рекомендую вам освежить этот пример в памяти, потому что сейчас мы вернемся к этой теме, рассмотрев безобидные, на первый взгляд, модификации примера из правила 24. Отличие только в том, что и класс Rational, и operator* в нем сделаны шаблонами:


    template <typename T>

    class Rational {

    public:

    Rational(const T& numerator = 0, // см. в правиле 20 – почему

    const T& denominator = 1); // параметр передается по ссылке

    const T numerator() const; // см. в правиле 28 – почему

    const T denominator() const; // результат возвращается по

    ... // значению, а в правиле 3 –

    // почему они константны

    };

    template <typename T>

    const Rational<T> operator*(const Rational<T>& lhs,

    const Rational<T>& rhs)

    {...}


    Как и в правиле 24, мы собираемся поддерживать смешанную арифметику, поэтому хотелось бы, чтобы приведенный ниже код компилировался. Мы не ожидаем подвохов, потому что аналогичный код в правиле 24 работал. Единственное отличие в том, что класс Rational и функция-член operator* теперь шаблоны:


    Raional<int> oneHalf(1, 2); // это пример из правила 24,

    // но Rational – теперь шаблон

    Ratinal<int> result = oneHalf * 2; // ошибка! Не компилируется


    Тот факт, что этот код не компилируется, наводит на мысль, что в шаблоне Rational есть нечто, отличающее его от нешаблонной версии. И это на самом деле так. В правиле 24 компилятор знал, какую функцию мы пытаемся вызвать (operator*, принимающую два параметра типа Rational), здесь же ему об этом ничего не известно. Поэтому компилятор пытается решить, какую функцию нужно конкретизировать (то есть создать) из шаблона operator*. Он знает, что имя этой функции operator* и она принимает два параметра типа Rational<T>, но для того чтобы произвести конкретизацию, нужно выяснить, что такое T. Проблема в том, что компилятор не может этого сделать.

    Пытаясь вывести T, компилятор смотрит на типы аргументов, переданных при вызове operator*. В данном случае это Rational<int> (тип переменной oneHalf) и int (тип литерала 2). Каждый параметр рассматривается отдельно.

    Вывод на основе типа oneHalf сделать легко. Первый параметр operator* объявлен как Rational<T>, а первый аргумент, переданный operator* (oneHalf), имеет тип Rational<int>, поэтому T должен быть int. К сожалению, вывести тип другого параметра не так просто. Из объявления известно, что тип второго параметра operator* равен Rational<T>, но второй аргумент, переданный функции operator* (число 2), имеет тип int. Как компилятору определить, что есть T в данном случае? Можно ожидать, что он воспользутся не-explicit конструктором, чтобы преобразовать 2 в Rational<int> и таким образом сделать вывод, что T есть int, но на деле этого не происходит. Компилятор не поступает так потому, что функции неявного преобразования типа никогда не рассматриваются при выводе аргументов шаблона. Никогда. Да, такие преобразования используются при вызовах функций, но перед тем, как вызывать функцию, нужно убедиться, что она существуют. Чтобы убедиться в этом, необходимо вывести типы параметров для всех потенциально подходящих шаблонов функций (чтобы можно было конкретизировать правильную функцию). Но неявные преобразования типов посредством вызова конструкторов при выводе аргументов шаблона не рассматриваются. В правиле 24 никаких шаблонов не было, поэтому и проблема вывода аргументов шаблона не возникала. Здесь же мы имеем дело с шаблонной частью C++ (см. правило 1), и она выходит на первый план.

    Мы можем помочь компилятору в выводе аргументов шаблона, воспользовавшись объявлением дружественной функции в шаблонном классе. Это означает, что класс Rational<T> может объявить operator* для Rational<T> как функцию-друга. К шаблонам классов процедура вывода аргументов не имеет отношения (она применяется только к шаблонам функций), поэтому тип T всегда известен в момент конкретизации Rational<T>. Это упрощает объявление соответствующей функции operator* как друга класса Rational<T>:


    template <typename T>

    class Rational {

    public:

    ...

    friend // объявление функции

    const Rational operator*(const Rational& lhs, // operator*

    const Rational& rhs); // (подробности см. ниже)

    };

    template <typename T> // определение функции

    const Rational<T> operator*(const Rational<T>& lhs, // operator*

    const Rational<T>& rhs)

    {...}


    Теперь вызовы operator* с аргументами разных типов скомпилируются, потому что при объявлении объект oneHalf типа Rational<int> конкретизируется класс Rational<int> и вместе с ним функция-друг operator*, которая принимает параметры Rational<int>. Поскольку объявляется функция (а не шаблон функции), компилятор может для вывода типов параметров пользоваться функциями неявного преобразования (например, не-explicit конструкторами Rational) и, стало быть, сумеет разобраться в вызове operator* с параметрами разных типов.

    К сожалению, фраза «сумеет разобраться» в данном контексте имеет иронический оттенок, поскольку хотя код и компилируется, но не компонуется. Вскоре мы займемся этой проблемой, но сначала я хочу сделать одно замечание о синтаксисе, используемом для объявления функции operator* в классе Rational.

    Внутри шаблона класса имя шаблона можно использовать как сокращенное обозначение шаблона вместе с параметрами, поэтому внутри Ratonal<T> разрешается писать просто Rational вместо Ratonal<T>. В данном примере это экономит лишь несколько символов, но когда есть несколько параметров с длинными именами, это помогает уменьшить размер исходного кода и одновременно сделать его яснее. Я вспомнил об этом, потому что operator* объявлен как принимающий и возвращающий Rational вместо Rational<T>. Также корректно было бы объявить operator* следующим образом:


    template <typename T>

    class Rational {

    public:

    ...

    friend

    const Rational<T> operator*(const Rational<T>& lhs,

    const Rational<T>& rhs);

    ...

    };


    Однако проще (и часто так и делается) использовать сокращенную форму.

    Теперь вернемся к проблеме компоновки. Код, содержащий вызов с параметрами различных типов, компилируется, потому что компилятор знает, что мы хотим вызвать вполне определенную функцию (operator*, принимающую параметры типа Rational<int> и Rational<int>), но эта функция только объявлена внутри Rational, но не определена там. Наша цель – заставить шаблон функции operator*, не являющейся членом класса, предоставить это определение, но таким образом ее не достичь. Если мы объявляем функцию самостоятельно (а так и происходит, когда она находится внутри шаблона Rational), то должны позаботиться и об ее определении. В данном случае мы нигде не привели определения, поэтому компоновщик его и не находит.

    Простейший способ исправить ситуацию – объединить тело operator* с его объявлением:


    template <typename T>

    class Rational {

    public:

    ...

    friend Rational operator*(const Rational& lhs, const Rational& rhs)

    {

    return Rational(lhs.numerator() * rhs.numerator(), // та же

    lhs.denominator () * rhs.denominator()); // реализация,

    } // что и

    // в правиле 24

    };


    Наконец-то все работает как нужно: вызовы operator* с параметрами смешанных типов компилируются, компонуются и запускаются. Ура!

    Интересное наблюдение, касающееся этой техники: использование отношения дружественности никак не связано с желанием получить доступ к закрытой части класса. Чтобы сделать возможными преобразования типа для всех аргументов, нам нужна функция, не являющаяся членом (см. правило 24); а для того чтобы получить автоматическую конкретизацию правильной функции, нам нужно объявить ее внутри класса. Единственный способ объявить свободную функцию внутри класса – сделать ее другом (friend). Что мы и делаем. Необычно? Да. Эффективно? Вне всяких сомнений.

    Как объясняется в правиле 30, функции, определенные внутри класса, неявно объявляются встроенными; это касается и функций-друзей, подобных нашей operator*. Вы можете минимизировать эффект от неявного встраивания, сделав так, чтобы operator* не делала ничего, помимо вызова вспомогательной функции, определенной вне класса. В данном случае в этом нет особой необходимости, потому что функция operator* и так состоит всего из одной строки, но для более сложных функций с телом это может оказаться желательным. Поэтому стоит иметь в виду идиому «иметь друга, вызывающего вспомогательную функцию».

    Тот факт, что Rational – это шаблонный класс, означает, что вспомогательная функция обычно также будет шаблоном, поэтому код в заголовочном файле, определяющем Rational, обычно выглядит примерно так:


    template <typename T> class Ratonal; // объявление

    // шаблона Rational

    template <typename T> // объявление

    const Rational<T> doMultiply(const Rational<T>& lhs, // шаблона

    const Rational<T>& rhs); // вспомогательной

    // функции

    template <typename T>

    class Rational {

    public:

    ...

    friend

    const Rational operator*( const Rational& lhs,

    const Rational& rhs) // друг объявляет

    { return doMultiply(lhs, rhs};} // вспомогательную

    ... // функцию

    };


    Многие компиляторы требуют, чтобы все определения шаблонов находились в заголовочных файлах, поэтому может понадобиться определить в заголовке еще и функцию doMultiply. Как объясняется в правиле 30, такие шаблоны не обязаны быть встроенными. Вот как это может выглядеть:


    template <typename T> // определение шаблона

    const Rational<T> doMultiply( const Rational<T>& lhs, // вспомогательной

    const Rational<T>& rhs) // функции

    { // в заголовочном файле

    return Rational(lhs.numerator() * rhs.numerator(), // при необходимости

    lhs.denominator () * rhs.denominator());

    }


    Конечно, будучи шаблоном, doMultiply не поддерживает умножения значений разного типа, но ей это и не нужно. Она вызывается только из operator*, который обеспечивает поддержку параметров смешанного типа! По существу, функция operator* поддерживает любые преобразования типа, необходимые для перемножения объектов класса Rational, а затем передает эти два объекта соответствующей конкретизации шаблона doMultiply, которая и выполняет собственно операцию умножения. Кооперация в действии, не так ли?

    Что следует помнить

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

    Правило 47: Используйте классы-характеристики для предоставления информации о типах

    В основном библиотека STL содержит шаблоны контейнеров, итераторов и алгоритмов, но есть в ней и некоторые служебные шаблоны. Один из них называется advance. Шаблон advance перемещает указанный итератор на заданное расстояние:


    template <typename T, typename DistT> // перемещает итератор iter

    void advance(Iter T& iter, DistT d); // на d элементов вперед

    // если d < 0, то перемещает iter

    // назад


    Концептуально advance делает то же самое, что предложение iter+=d, но таким способом advance не может быть реализован, потому что только итераторы с произвольным доступом поддерживают операцию +=. Для менее мощных итераторов advance реализуется путем повторения операции ++ или – ровно d раз.

    А вы не помните, какие есть категории итераторов в STL? Не страшно, дадим краткий обзор. Существует пять категорий итераторов, соответствующих операциям, которые они поддерживают. Итераторы ввода ( input iterators) могут перемещаться только вперед, по одному шагу за раз, и позволяют читать только то, на что они указывают в данный момент, причем прочитать значение можно лишь один раз. Они моделируют указатель чтения из входного файла. К этой категории относится библиотечный итератор C++ iostream_iterator. Итераторы вывода (output iterators) устроены аналогично, но служат для вывода: перемещаются только вперед, по одному шагу за раз, позволяют записывать лишь в то место, на которое указывают, причем записать можно только один раз. Они моделируют указатель записи в выходной файл. К этой категории относится итератор ostream_iterator. Это самые «слабые» категории итераторов. Поскольку итераторы ввода и вывода могут перемещаться только в прямом направлении и позволяют лишь читать или писать туда, куда указывают, причем лишь единожды, они подходят только для однопроходных алгоритмов.

    Более мощная категория итераторов состоит из однонаправленных итераторов (forward iterators). Такие итераторы могут делать все, что делают итераторы ввода и вывода, плюс разрешают читать и писать в то место, на которое указывают, более одного раза. Это делает их удобными для многопроходных алгоритмов. STL не предоставляет реализацию однонаправленных связных списков, но в некоторых библиотеках они есть (и обычно называются slist); итераторы таких контейнеров являются однонаправленными. Итераторы кэшированных контейнеров в библиотеке TR1 (см. правило 54) также могут быть однонаправленными.

    Двунаправленные итераторы (bidirectional iterators) добавляют к функциональности однонаправленных итераторов возможность перемещения назад. Итераторы для STL-контейнера list относятся к этой категории, равно как и итераторы для set, multiset, map и multimap.

    Наиболее мощная категория итераторов – это итераторы с произвольным доступом (random access iterators). Итераторы этого типа добавляют к функциям двунаправленных итераторов «итераторную арифметику», то есть возможность перемещения вперед и назад на заданное расстояние, затрачивая на это постоянное время. Такая арифметика аналогична арифметике указателей, что неудивительно, поскольку итераторы с произвольным доступом моделируют встроенные указатели, а встроенные указатели могут вести себя как итераторы с произвольным доступом. Итераторы для vector, deque и string являются итераторами с произвольным доступом.

    Для каждой из пяти категорий итераторов C++ в стандартной библиотеке имеется соответствующая «структура-тэг» (tag struct):


    struct input_iterator_tag {};

    struct output_iterator_tag {};

    struct forward_iterator_tag: public input_iterator_tag {};

    struct bidirectional_iterator_tag: public forward_iterator_tag {};

    struct random_access_iterator_teg: public bidirectional_iterator_tag {};


    Отношения наследования между этими структурами корректно выражают взаимосвязь типа «является» (см. правило 32): верно, что все однонаправленные итераторы являются также итераторами ввода и т. д. Вскоре мы увидим примеры использования такого наследования.

    Но вернемся к операции advance. Поскольку у разных итераторов возможности различны, то можно было при реализации advance воспользоваться «наименьшим общим знаменателем», то есть организовать цикл, в котором итератор увеличивается или уменьшается на единицу. Но такой подход требует линейных затрат времени. Итераторы с произвольным доступом обеспечивают доступ к любому элементу контейнера за постоянное время, и, конечно, мы бы хотели воспользоваться этим преимуществом, коль скоро оно имеется.

    В действительности хотелось бы реализовать advance как-то так:


    template<typename IterT, typename DistT>

    void advance(IterT& iter, DistT d)

    {

    if (iter является итератором с произвольным доступом) {

    iter += d; // использовать итераторную арифметику

    } // для итераторов с произвольным доступом

    else {

    if(d>=0) {while (d–) ++iter;} // вызывать ++ или – в цикле

    else {while(d++) –iter;} // для итераторов других категорий

    }

    }


    Но для этого нужно уметь определять, является ли iter итератором с произвольным доступом, что, в свою очередь, требует знания о том, что его тип – IterT – относится к категории итераторов с произвольным доступом. Другими словами, нам нужно получить некоторую информацию о типе. Именно для этого и служат характеристики (traits): получить информацию о типе во время компиляции.

    Traits – это не ключевое слово и не предопределенная конструкция в C++; это техника и соглашение, которому следуют программисты. Одним из требований, предъявляемых к ней, является то, что она должна одинаково хорошо работать и для встроенных типов, и для типов, определяемых пользователем. Например, при вызове для обычного указателя (типа const char*) или значения типа int операция advance должна работать, а это значит, что техника характеристик должна быть применима и к встроенным типам.

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


    template<typename IterT> // шаблон для информации

    struct iterator_traits; // о типах итераторов


    Как видите, iterator_traits – это структура. По соглашению характеристики всегда реализуются в виде структур. Другое соглашение заключается в том, что структуры, используемые для их реализации, почему-то называются классами- характеристиками.

    Смысл iterator_traits состоит в том, что для каждого типа IterT определяется псевдоним typedef iterator_category для структуры iterator_traits<IterT>. Этот typedef идентифицирует категорию, к которой относится итератор IterT.

    Реализация этой идеи в iterator_traits состоит из двух частей. Первая – вводится требование, чтобы все определяемые пользователем типы итераторов имели внутри себя вложенный typedef с именем iterator_category, который задает соответствующую структуру-тэг. Например, итераторы deque являются итераторами с произвольным доступом, поэтому класс итераторов deque должен выглядеть примерно так:


    template <…>

    class deque {

    public:

    class iterator {

    public:

    typedef random_access_iterator_tag iterator_category;

    ...

    };

    ...

    };


    Итераторы для контейнеров list являются двунаправленными, поэтому для них объявление выглядит так:


    template <…>

    class list {

    public:

    class iterator {

    public:

    typedef bidirectional_iterator_tag iterator_category;

    };

    ...

    };


    В шаблоне iterator_traits просто повторен находящийся внутри класса итератора typedef:


    // iterator_category для типа IterT – это то, что сообщает о нем сам IterT

    // см. в правиле 42 информацию об использовании “typedef typename”

    template <typename IterT>

    struct iterator_traits {

    typedef typename IterT::iterator_category iterator_category;

    ...

    };


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

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


    template <typename IterT> // частичная специализация шаблона

    struct iterator_traits<IterT*> // для встроенных типов указателей

    {

    typedef random_access_iterator_tar iterator_category;

    ...

    };


    Теперь вы должны понимать, как проектируется и реализуется класс-характеристика:

    • Идентифицировать информацию о типе, которую вы хотели бы сделать доступной (например, для итераторов – это их категория).

    • Выбрать имя для обозначения этой информации (например, iterator_category).

    • Предоставить шаблон и набор его специализаций (например, iterator_traits), которые содержат информацию о типах, которые вы хотите поддерживать.

    Имея шаблон iterator_traits, – на самом деле std::iterator_traits, потому что он является частью стандартной библиотеки C++, – мы можем уточнить наш псевдокод для advance:


    template<typename IterT, typename DistT>

    void advance(IterT& iter, DistT d)

    {

    if (typeid(typename std::iterator_traits<IterT>::iterator_category)==

    typeid(std::random_access_iterator_tag))

    ...

    }


    Выглядит многообещающе, но это не совсем то, что нужно. Во-первых, возникнет проблема при компиляции, но ее мы рассмотрим в правиле 48; а пока остановимся на более фундаментальном обстоятельстве. Тип IterT известен на этапе компиляции, поэтому iterator_traits<IterT>::iterator_category также может быть определен во время компиляции. Но предложение if вычисляется во время исполнения. Зачем делать во время исполнения нечто такое, что можно сделать во время компиляции? Это пустая трата времени и раздувание исполняемого кода.

    Что нам нужно – так это условная конструкция (например, предложение if..else) для типов, которая вычислялась бы во время компиляции. К счастью, в C++ есть необходимые нам средства. Это не что иное, как перегрузка.

    Когда вы перегружаете некоторую функцию f, вы указываете параметры разных типов для различных версий. Когда вызывается f, компилятор выбирает наиболее подходящую из перегруженных версий, основываясь на переданных аргументах. Компилятор, по сути, говорит: «Если эта версия лучше всего соответствует переданным параметрам, вызову ее; если лучше подходит другая версия – остановлюсь на ней, и так далее». Видите? Условная конструкция для типов во время компиляции. Чтобы заставить advance работать нужным нам образом, следует всего лишь создать две версии перегруженной функции, объявив в качестве параметра для каждой из них объекты iterator_category разных типов. Я назову эти функции doAdvance:


    template<typename IterT, typename DistT> // использовать эту

    void doAdvance(IterT& iter, DistT d, // реализацию для

    std::random_access_iterator_tag) // итераторов

    { // с произвольным доступом

    iter += d;

    }

    template<typename IterT, typename DistT> // использовать эту

    void doAdvance(IterT& iter, DistT d, // реализацию для

    std::bidirectional_iterator_tag) // двунаправленных

    { // итераторов

    if(d >= 0) {while(d–) ++iter;}

    else {while (d++) –iter;}

    }

    template<typename IterT, typename DistT> // использовать

    void doAdvance(IterT& iter, DistT d, // эту реализацию

    std::input_iterator_tag) // для итераторов

    { // ввода

    if(d < 0) {

    throw std::out_of_range(“Отрицательное направление”); // см. ниже

    }

    while (d–) ++iter;

    }


    Поскольку forward_iterator_tag наследует input_iterator_tag, то версия do-Advance для input_iterator_tag будет работать и с однонаправленными итераторами. Это дополнительный аргумент в пользу наследования между разными структурами iterator_tag. Фактически это аргумент в пользу любого открытого наследования: иметь возможность писать код для базового класса, который будет работать также и для производных от него классов.

    Спецификация advance допускает как положительные, так и отрицательные значения сдвига для итераторов с произвольным доступом и двунаправленных итераторов, но поведение не определено, если вы попытаетесь сдвинуть на отрицательное расстояние итератор ввода или однонаправленный итератор. Реализации, которые я проверял, просто предполагают, что d – не отрицательно, поэтому входят в очень длинный цикл, пытаясь отсчитать «вниз» до нуля, если им передается отрицательное значение. В коде, приведенном выше, я показал вариант, в котором вместо этого возбуждается исключение. Обе реализации корректны. Это проклятие неопределенного поведения: вы не можете предсказать, что произойдет.

    Имея разные перегруженные версии doAdvance, функции advance остается только вызвать их, передав в качестве дополнительного параметра объект, соответствующий типу категории итератора, чтобы компилятор мог применить механизм разрешения перегрузки для вызова правильной реализации:


    template <typename IterT, typename DistT>

    void advance(IterT& iter, DistT d)

    {

    doAdvance( // вызвать версию

    iter, d, // doAdvance

    typename // соответствующую

    std::iterator_traits<IterT>::iterator_category() // категории

    ); // итератора iter

    }


    Подведем итоги – как нужно использовать класс-характеристику:

    • Создать набор перегруженных «рабочих» функций либо шаблонов функций (например, doAdvance), которые отличаются параметром-характеристикой. Реализовать каждую функцию в соответствии с переданной характеристикой.

    • Создать «ведущую» функцию либо шаблон функции (например, advance), которая вызывает рабочие функции, передавая информацию, предоставленную классом-характеристикой.

    Классы-характеристики широко используются в стандартной библиотеке. Так, класс iterator_traits, помимо iterator_category, представляет еще четыре вида информации об итераторах (наиболее часто используется value_type; в правиле 42 показан пример его применения). Есть еще char_traits, который содержит информацию о символьных типах, и numeric_limits, который хранит информацию о числовых типах, например минимальных и максимальных значениях и т. п. Имя numeric_limits немного сбивает с толку, поскольку нарушает соглашение, в соответствии с которыми имена классов-характеристик должны оканчиваться на «traits», но тут уж ничего не поделаешь, придется смириться.

    В библиотеке TR1 (см. правило 54) есть целый ряд новых классов-характеристик, которые предоставляют информацию о типах, включая is_fundamental<T> (где T – встроенный тип), is_array<T> (где T – тип массива) и is_base_of<T1,T2> (то есть является ли T1 тем же, что и T2, либо его базовым классом). Всего TR1 добавляет к стандартному C++ более 50 классов-характеристик.

    Что следует помнить

    • Классы-характеристики делают доступной информацию о типах во время компиляции. Они реализованы с применением шаблонов и их специализаций.

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

    Правило 48: Изучите метапрограммирование шаблонов

    Метапрограммирование шаблонов (template metaprogramming – TMP) – это процесс написания основанных на шаблонах программ на C++, исполняемых во время компиляции. На минуту задумайтесь об этом: шаблонная метапрограмма – это программа, написанная на C++, которая исполняется внутри компилятора C+ +. Когда TMP-программа завершает исполнение, ее результат – фрагменты кода на C++, конкретизированные из шаблонов, – компилируется как обычно.

    Если эта идея не поразила вас до глубины души, значит, вы недостаточно напряженно думали о ней.

    C++ не предназначался для метапрограммирования шаблонов, но с тех пор, как технология TMP была открыта в начале 90-х годов, она оказалась настолько полезной, что, вероятно, и в сам язык, и в стандартную библиотеку будут включены расширения, облегчающие работу с TMP. Да, TMP было именно открыто, а не придумано. Средства, лежащие в основе TMP, появились в C++ вместе с шаблонами. Нужно было только, чтобы кто-то заметил, как они могут быть использованы изобретательным и неожиданным образом.

    Технология TMP дает два преимущества. Во-первых, она позволяет делать такие вещи, которые иными способами сделать было бы трудно либо вообще невозможно. Во-вторых, поскольку шаблонные метапрограммы исполняются во время компиляции C++, они могут переместить часть работы со стадии исполнения на стадию компиляции. В частности, некоторые ошибки, которые обычно всплывают во время исполнения, можно было бы обнаружить при компиляции. Другое преимущество – это то, что программы C++, написанные с использованием TMP, можно сделать эффективными почти во всех смыслах: компактность исполняемого, код быстродействия, потребления памяти. Но коль скоро часть работы переносится на стадию компиляции, то, очевидно, компиляция займет больше времени. Для компиляции программ, в которых применяется технология TMP, может потребоваться намного больше времени, чем для компиляции аналогичных программ, написанных без применения TMP.

    Рассмотрим псевдокод шаблонной функции advance, представленный на стр. 227 (см. правило 47; возможно, имеет смысл перечитать это правило сейчас, поскольку ниже я предполагаю, что вы знакомы с изложенным в нем материалом). Я выделил в этом фрагменте часть, написанную на псевдокоде:


    template<typename IterT, typename DistT>

    void advance(IterT& iter, DistT d)

    {

    if (iter является итератором с произвольным доступом) {

    iter += d; // использовать итераторную арифметику

    } // для итераторов с произвольным доступом

    else {

    if(d>=0) {while (d–) ++iter;} // вызывать ++ или – в цикле

    else {while(d++) –iter;} // для итераторов других категорий

    }

    }


    Мы можем использовать typeid, чтобы заменить псевдокод реальным кодом. Тогда задача будет решена «нормальным» для C++ способом – вся работа выполняется во время исполнения:


    template<typename IterT, typename DistT>

    void advance(IterT& iter, DistT d)

    {

    if (typeid(typename std::iterator_traits<IterT>::iterator_category)==

    typeid(std::random_access_iterator_tag))

    iter += d; // использовать итеративную арифметику

    } // для итераторов с произвольным доступом

    else {

    if(d>=0) {while (d–) ++iter;} // вызывать ++ или – в цикле

    else {while(d++) –iter;} // для итераторов других категорий

    }

    }


    В правиле 47 отмечено, что подход, основанный на typeid, менее эффективен, чем при использовании классов-характеристик, поскольку в этом случае: (1) проверка типа происходит во время исполнения, а не во время компиляции, и (2) код, выполняющий проверку типа, должен быть включен в исполняемую программу. Фактически этот пример показывает, как технология TMP может порождать более эффективные программы на C++, потому что характеристики – это и есть частный случай TMP. Напомню, что характеристики делают возможным вычисление выражения if…else во время компиляции.

    Я уже отмечал выше, что некоторые вещи технология TMP позволяет сделать проще, чем «нормальный» C++, и advance можно считать иллюстраций этого утверждения. В правиле 47 упоминается о том, что основанная на typeid реализация advance может привести к проблемам во время компиляции, и вот вам пример такой ситуации:


    std::list<int>::iterator iter;

    ...

    advance(iter, 10); // сдвинуть iter на 10 элементов вперед

    // не скомпилируется для приведенной

    // выше реализации


    Рассмотрим версию advance, которая будет сгенерирована для этого вызова. После подстановки типов iter и 10 в качестве параметров шаблона IterT и DistT мы получим следующее:


    void advance(std::list<int>::iterator& iter, int d)

    {

    if (typeid(std::iterator_traits<std::list<int>::iterator>::iterator_category)==

    typeid(std::random_access_iterator_tag))

    iter += d; // ошибка!

    }

    else {

    if(d>=0) {while (d–) ++iter;}

    else {while(d++) –iter;}

    }

    }


    Проблема в выделенной строке, где встречается оператор +=. В данном случае мы пытаемся использовать += для типа list<int>::iterator, но list<int>::iterator – это двунаправленный итератор (см. правило 47), поэтому он не поддерживает +=. Оператор += поддерживают только итераторы с произвольным доступом. Мы знаем, что никогда не попытаемся исполнить предложение, содержащее +=, потому что для list<int>::iterator проверка с привлечением typeid никогда не выполнится успешно, но компилятор-то обязан гарантировать, что весь исходный код корректен, даже если он никогда не исполняется, а «iter += d» – некорректный код в случае, когда iter не является итератором с произвольным доступом. Решение же на основе технологии TMP предполагает, что код для разных типов вынесен в разные функции, каждая из которых использует только операции, применимые к типам, для которых она написана.

    Было доказано, что технология TMP представляет собой полную машину Тьюринга, то есть обладает достаточной мощью для любых вычислений. Используя TMP, вы можете объявлять переменные, выполнять циклы, писать и вызывать функции и т. д. Но такие конструкции выглядят совершенно иначе, чем их аналоги из «нормального» C++. Например, в правиле 47 показано, как в TMP условные предложения if…else выражаются с помощью шаблонов и их специализаций. Но такие конструкции можно назвать «TMP уровня ассемблера». В библиотеках для работы с TMP (например, MPL из Boost – см. правило 55) предлагается более высокоуровневый синтаксис, хотя его также нельзя принять за «нормальный» С++.

    Чтобы взглянуть на TMP с другого боку, посмотрим, как там выглядят циклы. Технология TMP не предоставляет настоящих циклических конструкций, поэтому цикл моделируется с помощью рекурсии. (Если вы не очень уверенно владеете рекурсией, придется освоиться с ней прежде, чем приступать к использованию TMP. Ведь TMP – по существу функциональный язык, а для таких языков рекурсия – то же, что телевидение для американской поп-культуры – неотъемлемая принадлежность.) Но и рекурсия-то не совсем обычная, поскольку при реализации циклов TMP нет рекурсивных вызовов функций, а есть рекурсивные конкретизации шаблонов.

    Аналогом программы «Hello World» на TMP является вычисление факториала во время компиляции. Конечно, она, как и «Hello World», не поразит воображение, но обе полезны для демонстрации базовых возможностей языка. Вычисление факториала с помощью TMP сводится к последовательности рекурсивных конкретизаций шаблона. Кроме того, демонстрируется один из способов создания и использования переменных в TMP. Смотрите:


    template<unsigned n> // общий случай: значение Factorial<n> – это

    struct Factorial { // произведение n и Factorial<n-1>

    enum { value = n*Factorial<n-1>::value };

    };

    template<> // частный случай: значение Factorial<0> –

    struct Factorial<0> { // это 1

    enum { value = 1 };

    };


    Имея такую шаблонную метапрограмму (на самом деле просто единственную шаблонную метафункцию Factorial), вы получаете значение факториала n, обращаясь к Factorial<n>::value.

    Циклическая часть кода возникает там, где конкретизация шаблона Factorial<n> ссылается на конкретизацию шаблона Factorial<n-1>. Как во всякой рекурсивной программе, здесь есть особый случай, прекращающий рекурсию. В данном случае это специализация шаблона Factorial<0>.

    Каждая конкретизация шаблона Factorial является структурой struct, и в каждой структуре используется «трюк с перечислением» (см. правило 2) для объявления переменной TMP с именем value. В переменной value хранится текущее значение факториала. Если бы в TMP были настоящие циклы, то значение value обновлялось бы на каждой итерации цикла. Но поскольку в TMP место циклов заменяет рекурсивная конкретизация шаблонов, то каждая конкретизация получает свою собственную копию value, и значение копии соответствует «итерации цикла».

    Использовать Factorial можно следующим образом:


    int main()

    {

    std::cout << Factorial<5>::value; // печатается 120

    std::cout << Factorial<10>::value; // печатается 3628800

    }


    Если вы находите описанный прием элегантным, значит, вы стали на путь превращения в метапрограммиста шаблонов. Если же все эти шаблоны, специализации, рекурсивные конкретизации, трюк с перечислением и необходимость набирать нечто вроде Factorial<n-1>::value не вызывают у вас восторга, стало быть, вы вполне нормальный программист C++.

    Конечно, шаблон Factorial в такой же мере демонстрирует полезность TMP, как «Hello World» – полезность любого обычного языка программирования. Чтобы понять, почему о TMP стоит знать, важно представлять себе, чего можно достичь с помощью этой технологии. Вот три примера:

    Обеспечение корректности единиц измерения. В научных и инженерных приложениях важно, чтобы единицы измерения (например, массы, расстояния, времени и т. п.) правильно сочетались. Присваивание переменной, представляющей массу, значения переменной, представляющей скорость, – это ошибка, но деление переменной расстояния на переменную времени и присваивание результата переменной скорости правильно. Используя TMP, можно обеспечить (во время компиляции), что все комбинации единиц измерения в программе будут корректны, независимо от того, насколько сложны вычисления. (Это пример того, как можно использовать TMP для ранней диагностики ошибок.) Одним интересным аспектом такого использования TMP может быть поддержка вычисления дробных степеней. Смысл в том, чтобы дроби сокращались во время компиляции, то есть чтобы компилятор мог подтвердить, например, что единица времени в степени 1/2 – это то же самое, что единица времени в степени 4/8.

    Оптимизация операций с матрицами. В правиле 21 объясняется, что некоторые функции, включая operator*, должны возвращать новые объекты, а в правиле 44 представлен класс SquareMatrix, поэтому рассмотрим такой код:


    typedef SquareMatrix<double, 10000> BigMatrix;

    BigMatrix m1, m2, m3, m4, m5; // создать матрицы

    ... // и присвоить им значения

    BigMatrix result = m1 * m2 * m3 * m4 * m5; // вычислить произведение


    Вычисление result «нормальным» способом приводит к созданию четырех временных матриц, по одной для каждого вызова operator*. Более того, независимые операции умножения порождают последовательность из четырех циклов по элементам матрицы. Но применение передовой шаблонной технологии, тесно связанной с TMP и получившей название шаблоны выражений (expression templates), позволяет избежать создания временных объектов и объединить циклы, причем все это без изменения приведенного выше пользовательского кода. В результате программа требует меньше памяти и выполняется значительно быстрее.

    • Генерация специализированных реализаций паттернов проектирования. Паттерны проектирования, подобные Strategy (см. правило 35), Observer, Visitor и т. п., могут быть реализованы многими способами. Используя основанную на TMP технологию, называемую проектирование на основе политик (policy-based design), можно создавать шаблоны, представляющие независимые проектные решения («политики»), которые могут быть соответствующим образом скомбинированы для порождения реализаций паттернов с заданным поведением. Например, эта техника применялась для того, чтобы из нескольких шаблонов, реализующих различное поведение «интеллектуальных» указателей, породить (во время компиляции) любой из сотен разных типов «интеллектуальных» указателей. В результате обобщения, выходящего за рамки привычных программных конструкций, к примеру паттернов проектирования и «интеллектуальных» указателей, эта технология ложится в основу так называемого порождающего программирования (generative programming).

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

    Поддержка TMP растет. Вероятно, в следующей версии C++ будет реализована явная поддержка этой технологии, в TR1 это уже декларировано (см. правило 54). Начали появляться книги, посвященные этой теме, а информация о TMP в Internet становится все богаче. Видимо, TMP никогда не станет главным направлением развития, но для некоторых программистов (особенно разработчиков библиотек) она почти наверняка займет важное место.

    Что следует помнить

    • Метапрограммирование шаблонов позволяет перенести часть работы со стадии исполнения на стадию компиляции. За счет этого можно раньше обнаружить ошибки и повысить производительность программ.

    • Технология TMP может быть использована для генерации кода на основе комбинации политик, а также чтобы предотвратить генерацию кода, некорректного для определенных типов данных.









    Главная | Контакты | Нашёл ошибку | Прислать материал | Добавить в избранное

    Все материалы представлены для ознакомления и принадлежат их авторам.