четверг, 11 ноября 2010 г.

Шаблонные методы без аргументов и неизвестный смысл слова template

Язык С++ не перестаёт удивлять меня. Изучаю его давно, тем не менее, периодически обнаруживаются  неизведанные уголки. Есть ощущение, что C++ так же неисчерпаем, как и атом.

Как известно, внутри классов можно объявлять шаблонные статические методы:

struct Foo {
   template <class T>
   static int method() { return 0; };
};

Теперь можно написать шаблонную функцию f, которая для заданного параметра T зовет соответствующий method<T> из класса Foo:

template<class T>
int f() { return Foo::method<T>(); }

Попробуем теперь параметризовать эту функцию еще классом S, из которого зовется метод, и подставить Foo вместо S:

template<class S, class T>
int f() { return S::method<T>(); }

...

int i = f<Foo, int>();

Что же мы видим? Наш любимый g++ выдает ошибку:

test.cpp: error: expected primary-expression before '>' token
test.cpp: error: expected primary-expression before ')' token

Оказывается, нельзя вызывать шаблонный статический метод с явным указанием типа, когда класс-владелец метода сам является шаблонным параметром.
Собственно, можно даже в функции f отказаться от параметра T, положив его равным, скажем, int:

template<class S>
int f() { return S::method<int>(); }

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

struct Foo {
   template <class T>
   static int method(T t) { return 0; };
};

template<class S, class T>
int f() { T t; return S::method(t); }

В этом фрагменте уже всё нормально, компилируется. Но нам отнюдь не всегда нужны аргументы в методе. Не передавать же их только для того, чтобы заставить компилятор нормально работать...
Проблема также исчезает, когда мы зовем метод не из самого класса-параметра S, а из какого-то другого класса Traits, зависящего от S:

template<class S> 
struct Traits {
   template <class T>
   static int method() { return 0; };
};

template<class S, class T>
int f() { return Traits<S>::method<T>(); }

Собственно, это один из способов преодолеть трудность - использовать зависимый класс Traits<S> вместо S для хранения метода, зависящего от T.

Аналогичная проблема проявляется при обращении к вложенному шаблонному классу:

struct Foo {
   template<class T>
   struct Bar {};
};

template <class T>
void g() { Foo::Bar<T> bar; }

Это компилируется. А следующий пример - нет:

template <class S, class T>
void g() { typename S::Bar<T> bar; }

Даже более простой случай без T не компилируется:

template <class S>
void g() { typename S::Bar<int> bar; }

Ошибка та же, но сообщение об ошибке уже немного другое:

test.cpp: error: expected primary-expression before '>' token
test.cpp: error: expected primary-expression before ')' token
... (дописать)

Кстати, именно этот вариант сообщения об ошибке навел меня на "правильное" решение проблемы.

Итак, внимание, правильный ответ. Нужно использовать ключевое слово template в непривычном для нас месте: 

template<class S, class T>
int f() { return S:: template method<T>(); }

template<class S, class T>
void g() { typename S:: template Bar<T> bar; }

Надо отметить, что в VisualStudio 2008 указанная проблема не наблюдается - там слово template можно писать, а можно нет, всё и так компилируется. (Вопрос: может, там и typename писать не надо?)

"Когда такое нужно?" - спросите Вы. Ну, напрмер, мы имеем дело с различными потоками ввода-вывода, где статический шаблонный метод сообщает нам размер буфера, отводимого в этом потоке при чтении записей заданного типа:

struct MyStream {
   template <class Record>
   static int bufferSize() { return 1024; }
};

template<class Stream, class Record>
int streamBufferSize() { return Stream::template bufferSize<T>(); }

...

int *p = new char[ streamBufferSize<MyStream, int>() ];

Либо можно прибегнуть к вспомогательному классу Traits:

template <class Stream, class Record>
struct Traits {
   static int bufferSize() { return 1024; }
};


...

int *p = new char[ Traits<MyStream, int>::bufferSize() ];

Но этот (универсальный) способ требует дополнительных телодвижений и усложняет код.

Какие есть теоретические обоснования рассмотренному тут феномену, мне на данный момент неизвестно.

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

  1. Уххх, спасибо, значит, это не у меня глюки: довольно больно наступил на грабли с вызовом шаблонной статической функции у класса, являющегося параметром шаблона. Придётся теперь перепахать кусок разрабатываемой библиотеки, чтобы как-то уйти от проблемы. А так казалось всё красиво и заманчиво...

    ОтветитьУдалить
  2. Этот комментарий был удален автором.

    ОтветитьУдалить
  3. Спасибо, для моего случая подошло отлично! Для методов класса тоже работает. Пример на ideone.

    ОтветитьУдалить
  4. Спасибо, как раз то что панадобилось, а то на VS идет а на другие платформы нет.

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