*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***

пятница, 13 февраля 2009 г.

Шаблоны как параметры шаблона

Есть в шаблонах С++ интересная возможность параметризировать сами параметры шаблонов. Чтобы это могло значить?

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

template< typename C, typename E >
void print(const C<E>& v) {
copy(v.begin(), v.end(), ostream_iterator<E>(cout, " "));
cout << endl;
}
И все бы ничего, но с только зрения синтаксиса С++ это неверно. Нельзя просто написать C<E>, если E сам является не определенным типом, а параметром шаблона. Правильный способ использования параметра шаблона, который в свою очередь зависит от другого параметра, должен выглядеть так:
template< template<typename> class C, typename E >
void print(const C<E>& v) {
copy(v.begin(), v.end(), ostream_iterator<E>(cout, " "));
cout << endl;
}
Теперь полный пример (файл template_parameter.cpp):
#include <iostream>
#include <iomanip>
#include <algorithm>
#include <iterator>
#include <string>
#include <vector>
#include <list>
#include <deque>

// Я обычно не использую пространства имен "по умолчанию", но тут
// это сделано для компактности примера.
using namespace std;

// Вся изюминка тут: template<typename> или template<class>.
// Без этого параметр шаблона "С" нельзя будет параметризировать.
// в конструкции C<E>&.
template< template<typename> class C, typename E >
// Тут происходит параметризация параметра "С" параметром "E".
// Без этого класс "С" не может быть использован, так как "E"
// является не просто типом, а тоже параметром шаблона.
void print(const C<E>& v) {
// Так как класс элемента контейнера "Е" нам тут нужен как отдельный
// тип, то для этого и затеяна вся тема с параметризированными
// параметрами шаблона.
copy(v.begin(), v.end(), ostream_iterator<E>(cout, " "));
cout << endl;
}

// Тестовая программа демонстрирует, как одна функция print()
// может использоваться для печати любого контейнера
// (если, конечно, он удовлетворяет требованиям алгоритма
// copy() по наличию должных итераторов), содержащего элементы
// любого типа.
int main(int argc, char* argv[]) {
// Массив целых.
int i[5] = { 1, 2, 3, 4, 5 };
// Создаем вектор, состоящий из целых, и печатаем его.
print< vector, int >( vector<int>(i, i + 5) );

// Массив вещественных.
float f[5] = { .1, .2, .3, .4, .5 };
// Создаем вектор, состоящий из вещественных, и печатаем его.
print< vector, float >( vector<float>(f, f + 5) );

// Массив символов.
char c[5] = { 'a', 'b', 'c', 'd', 'e' };
// Создаем деку, состоящую их символов, и печатаем ее.
print< deque, char >( deque<char>(c, c + 5) );

// Массив строк в стиле С.
char* s[5] = { "a1", "b2", "c3", "d4", "e5" };
// Создаем список, состоящий из строк, и печатаем его.
print< list, string >( list<string>(s, s + 5) );

return 0;
}
Компилируем.

Cygwin:

g++ -o template_parameter_cygwin.exe template_parameter.cpp
или в Borland/Codegear Studio 2007:
bcc32 /etemplate_parameter_cg2007.exe template_parameter.cpp
И запускаем скомпилированный файл:
1  2  3  4  5
0.1 0.2 0.3 0.4 0.5
a b c d e
a1 b2 c3 d4 e5
Отчетливо видно, что на первой строке распечатаны целые, на второй вещественные, на третьей символы, и на четвертой строки.
Вы спросите, где компиляция в Visual Studio? А вот с ней вышел облом. Я пробовал скомпилировать этот пример в Visual Studio 2005 и 2008, и в обоих случаях я получал ошибки типа:
template_as_parameter.cpp(38) : error C3208: 'print' : template parameter list for class template 'std::vector' does not match template parameter list for template template parameter 'C'

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

Я был очень расстроен подобным фактом, так как в целом очень положительно отношусь к cl.exe. А тут выходит, что даже борландовый компилятор это понимает, а cl.exe нет. Если кто знает, может есть ключик какой секретный для включения поддержки "хитрых и редких" возможностей С++ в компиляторе микрософта — научите, пожалуйста. Буду очень признателен.
Предвосхищу вопросы типа "зачем так сложно, да еще и плохо переносимо" — все верно. Лично я бы отнес все выше описанное к "темным углам" С++, но уж больно интересно по ним полазать.

Обновление
Комментарий Александра прояснил ситуацию с проблемой при компиляции в Visual Studio. Окончательный вариант кода, чтобы работало в cl.exe, таков:
template< template<typename, typename> class C, typename E >
void print(const C<E, allocator<E> >& v) {
copy(v.begin(), v.end(), ostream_iterator<E>(cout, " "));
cout << endl;
}
У шаблонов стандартных контейнеров есть второй параметр, так называемый allocator. Этот параметр часто используется со значением по умолчанию, поэтому редко приходится вспоминать о нем. И как уточнил Александр, моя проблема была в том, что cl.exe требует явного указания наличия этого параметра при параметризации параметра C.

Исправленный код компилируется во всех опробованных компиляторах, теперь включая и cl.exe.


Другие посты по теме:

7 комментариев:

  1. Так вам же cl.exe ясно говорит - не совпадают параметры шаблона С (у вас один параметр - тип элемента коллекции) и параметры шаблона vector (он хочет 2 параметра - тип элемента и аллокатор).

    Можно написать вот так:
    template< template<typename, typename> class C, typename E >
    void print(const C<E, allocator<E>>& v) {...}
    В VC2005 это прекрасно компилируется и работает.

    Лучшим решением было бы сделать класс аллокатора параметром вашего шаблона и дать ему дефолтное значение = allocator<E>, но такие шалости к сожалению cl.exe не пропустит - дефолтные значения шаблонных параметров возможны только в шаблонах классов (но не функций как в вашем случае).

    ОтветитьУдалить
  2. Alexander: Большое спасибо за пояснение. Теперь все работает. Обновил материал для полноты картины. Мне сложно судить о "законности" требования cl.exe указывать этот параметр явно (если g++ и bcc32 могли вывести полное определение шаблона, значит грамматика это позволяет, и нет неоднозначности). Но в целом такое требование абсолютно некритично для "удобного" использования. У Страуструпа (специальное издание) я нашел только небольшой параграф (B.13.3) на эту тему, и там не раскрыто про параметры шаблонов, у которых есть значения по умолчанию -- надо их явно указывать в данном синтаксисе или нет.

    ОтветитьУдалить
  3. Ну да, с шаблонами дело немного тёмное, вернее с их поддержкой в разных компиляторах.

    Может читали - есть книга Андрея Александреску Modern C++ Design - там вообще используется Watcom C++ или что-то ещё более экзотичное. Остальные мол не годятся ))). Книга кстати - пожалуй лучшее что было написано про шаблоны.

    ОтветитьУдалить
  4. Alexander: Мне, к сожалению, не получается выбирать компиляторы: Windows - MSVC 2005/2008, Linux - gcc, Solaris - Sun Studio, HP-UX - aCC, AIX - XL C++ и т.д. И ни какого тебе gcc везде, и ни какого тебе Watcom'а. Вот тут и задумаешься о настоящей переносимости, так как софт должен работать везде. Например, от ACE framework пришлость отказаться и писать сокеты и работу с процессами самим, так как у ACE есть проблемы с компилятором на HP-UX, который не понимает "кое-чего" и т.д. Вообще, когда спектр рабочих платформ реально широк, то тут десять раз подумаешь, а связываться ли очередной новой примочкой. Хорошо в тем, у кого "идеальный" мир программирования сходится только к gcc, причем самой последней версии. Им и boost пожалуйста, и все прочие модные шаблонные примочки. Проще надо быть! ;-)

    ОтветитьУдалить
  5. приятно почитать!

    зато MSVC кушает такое:

    template<class Functor>
    class Repeating
    {
    public:
    template <>
    static void Repeat<0>( const Functor& f)
    {
    f(0);
    };

    template <const int Count>
    static void Repeat(const Functor& f)
    {
    f(Count);
    Repeat<Count - 1>(f);
    };
    };

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

    ОтветитьУдалить
  6. Очень полезная информация. Однако в Microsoft VStudio C++ 2006 имеются проблемы помимо специальной формы параметров шаблона. Компилятор выдает диагностику "template definitions cannot nest" Как быть с этой ошибкой компиляции ? Может быть нужен специальный параметр компилятору (например, оператор dynamic_cast компилируется, если в компиляторе задан ключ "/GR") ?

    ОтветитьУдалить
  7. AV: Не имею под рукой VS2006. Увы.

    ОтветитьУдалить