Технические проблемы использования шаблонов
С разработкой шаблонов классов и функций связан ряд неприятных технических проблем. Написать компилирующийся и инстанцирующийся со всем желаемым набором типов обобщенный код является намного более сложной задачей, чем написание обычной необобщенной версии алгоритма или структуры данных.
Первое, с чем сталкиваются при работе с шаблонами - это сложные тексты ошибок компиляции, если что-либо записано не так. Нередко, вместо аккуратного простого описания о произошедшей ошибке, при компиляции шаблонного кода может быть выдана серия абсолютно непонятных ошибок сложной структуры, в которых не просто разобраться. В литературе такое поведение компиляторов называют “ошибками-романами”, сетуя на их количество и длину. В популярной книге о технике использования шаблонов “Шаблоны С++: справочник разработчика” приведен пример текста сгенерированной компилятором ошибки, вызванного совсем небольшой оплошностью в коде вызова шаблона функции, обрабатывающей связные списки строк. Длина сообщения действительно впечатляет, но это далеко на худший случай, который может встретиться на практике:
/local/include/stl/_algo.h: In function 'struct _STL::_List_iterator<_STL::basic
_string<char,_STL::char_traits<char>,_STL::allocator<char> >,_STL::_Nonconst_tra
its<_STL::basic_string<char,_STL::char_traits<char>,_STL::allocator<char> > > >
_STL::find_if<_STL::_List_iterator<_STL::basic_string<char,_STL::char_traits<cha
r>,_STL::allocator<char> >,_STL::_Nonconst_traits<_STL::basic_string<char,_STL::
char_traits<char>,_STL::allocator<char> > > >, _STL::binder2nd<_STL::greater<int
> > >(_STL::_List_iterator<_STL::basic_string<char,_STL::char_traits<char>,_STL:
:allocator<char> >,_STL::_Nonconst_traits<_STL::basic_string<char,_STL::char_tra
its<char>,_STL::allocator<char> > > >, _STL::_List_iterator<_STL::basic_string<c
har,_STL::char_traits<char>,_STL::allocator<char> >,_STL::_Nonconst_traits<_STL:
:basic_string<char,_STL::char_traits<char>,_STL::allocator<char> > > >, _STL::bi
nder2nd<_STL::greater<int> >, _STL::input_iterator_tag)':
/local/include/stl/_algo.h:115: instantiated from '_STL::find_if<_STL::_List_i
terator<_STL::basic_string<char,_STL::char_traits<char>,_STL::allocator<char> >,
_STL::_Nonconst_traits<_STL::basic_string<char,_STL::char_traits<char>,_STL::all
ocator<char> > > >, _STL::binder2nd<_STL::greater<int> > >(_STL::_List_iterator<
_STL::basic_string<char,_STL::char_traits<char>,_STL::allocator<char> >,_STL::_N
onconst_traits<_STL::basic_string<char,_STL::char_traits<char>,_STL::allocator<c
har> > > >, _STL::_List_iterator<_STL::basic_string<char,_STL::char_traits<char>
,_STL::allocator<char> >,_STL::_Nonconst_traits<_STL::basic_string<char,_STL::ch
ar_traits<char>,_STL::allocator<char> > > >, _STL::binder2nd<_STL::greater<int>
>)'
testprog.cpp:18: instantiated from here
/local/include/stl/_algo.h:78: no match for call to '(_STL::binder2nd<_STL::grea
ter<int> >) (_STL::basic_string<char,_STL::char_traits<char>,_STL::allocator<cha
r> > &)'
/local/include/stl/_function.h:261: candidates are: bool _STL::binder2nd<_STL::g
reater<int> >::operator ()(const int &) const
Второй проблемой являются имена генерируемых экземпляров функций и методов. На уровне реализации, имена включают все аргументы шаблонов, и могут становиться достаточно длинными. Длину идентификаторов также можно увидеть и оценить в приведенном выше примере. В более ранних версиях компиляторов и компоновщиков при интенсивном использовании шаблонов иногда случалась довольно редкая для обычного программного кода ошибка превышения допустимой длины идентификаторов.
Существует также ряд синтаксических нюансов, которые нужно соблюдать при написании кода с шаблонами. Например, некоторые более ранние компиляторы (например, Visual Studio до версии 2010) плохо реагируют на идущие подряд закрывающие угловые скобки вложенных шаблонов. Такой код может не компилироваться, поскольку компилятор может воспринять последовательность символов “>>” как оператор сдвига, а не как 2 идущие подряд закрывающие угловые скобки:
std::vector< std::vector< int>> vv;
Проблему решают вставкой дополнительного пробела:
std::vector< std::vector< int> > vv;
// ^ стоит поставить пробел
Еще одной надоедливой синтаксической проблемой является использование вложенных имен, зависящих от аргумента шаблона. Предположим, реализация предполагает, чтобы все аргументы T в приведенном ниже шаблоне имели вложенный синоним типа value_type:
template< typenameT >
classTest
{
T::value_type x;
};
Поскольку компилятор ничего не знает о типе T в момент анализа кода, он не может определить является ли запись T::value_type типом или обращением к статическому члену с именем value_type. При компиляции будет выдана такая серия ошибок:
warning C4346: 'T::value_type' : dependent name is not a type
prefix with 'typename' to indicate a type
see reference to class template instantiation 'Test<T>' being compiled
error C2146: syntax error : missing ';' before identifier 'x'
error C4430: missing type specifier - int assumed. Note: C++ does not support default-int
Компилятор просто нуждается в пояснении, что зависимое от T вложенное имя value_type является типом данных, а не статическим членом, для чего код дополняют еще одним ключевым словом typename непосредственно перед использованием вложенного типа:
template< typenameT >
classTest
{
typenameT::value_type x;
};
Существуют и более неприятные синтаксические странности. Например, в коде иногда может потребоваться объяснить некоторым компиляторам, что речь идет об аргументах шаблона, а не об операторе <, добавив странно выглядящее синтаксическое средство “. template”:
classTest
{
public:
template< typenameT>
intf ( T _x );
};
template< typenameT >
voidf ( T _x )
{
Test t;
std::cout << t. templatef< T >( _x );
// Оригинально: std::cout << t.f< T >( _x );
}
Еще одной проблемой практического применения шаблонов является разница в поддержке между компиляторами. Очень часто возникают ситуации, когда код прекрасно компилируется на одном компиляторе, но выдает ошибки на другом. Это связано с отступлением конкретными компиляторами от норм стандартов, а также с дефектами в реализации самих компиляторов. Чтобы получить действительно переносимый код, иногда потребуется вносить исправления после реализации и регулярно собирать программу на нескольких компиляторах.