Безопасно ли размещать определение специализации функции-члена шаблона (без тела по умолчанию) в исходном файле?
Вот что я имею в виду:
// test.h
class cls
{
public:
template< typename T >
void f( T t );
};
-
// test.cpp
template<>
void cls::f( const char* )
{
}
-
// main.cpp
int main()
{
cls c;
double x = .0;
c.f( x ); // gives EXPECTED undefined reference (linker error)
const char* asd = "ads";
c.f( asd ); // works as expected, NO errors
return 0;
}
Это совершенно нормально, верно?
Я начал сомневаться в этом, потому что я просто переехал specialization of '...' after instantiation
ошибка, которая была новой для меня. Итак, я "обошел" эту ошибку, и теперь все работает нормально, но все же..
Это четко определенное поведение?
edit: И то же самое для функций-шаблонов, не являющихся членами (вперед объявленные функции-члены, не являющиеся членами)
3 ответа
Гонки Легкости в Орбите процитировали, почему это не соответствует стандарту. Там могут быть некоторые другие, в непосредственной близости.
Я попытаюсь объяснить более простыми словами, что означает стандартное словоблудие, и, надеюсь, я получу его правильно, и, наконец, объясню ошибки компоновщика (или отсутствие ошибок):
- Какой смысл в создании экземпляра?
- Как компилятор выбирает специализацию?
- Что необходимо в момент создания экземпляра?
- Почему ошибка компоновщика?
1 / В чем смысл создания экземпляра?
Точкой реализации функции шаблона является точка, в которой она вызывается или на которую ссылаются (&std::sort<Iterator>
) со всеми параметрами шаблона (*).
template <typename T>
void foo(T) { std::cout << typeid(T).name() << "\n"; }
int main() { foo(1); } // point of instantiation of "foo<int>(int)"
Однако это может быть отложено и, таким образом, не соответствует точному сайту вызова для шаблонов, вызываемых из других шаблонов:
template <typename T>
void foo(T) { std::cout << typeid(T).name() << "\n"; }
template <typename T>
void bar(T t) { foo(t); } // not a point of instantiation, T is still "abstract"
int main() { foo(1); } // point of instantiation of "bar<int>(int)"
// and ALSO of "foo<int>(int)"
Эта задержка очень важна, поскольку позволяет писать:
- ко-рекурсивные шаблоны (то есть шаблоны, которые ссылаются друг на друга)
- пользовательские специализации
(*) Грубо говоря, есть исключения, такие как не шаблонные методы шаблонного класса...
2 / Как компилятор выбирает специализацию?
В момент создания, компилятор должен иметь возможность:
- решить, какую функцию базового шаблона вызвать
- и, возможно, какую из его специализаций назвать
Этот старый GotW демонстрирует горе специализаций... но вкратце:
template <typename T> void foo(T); // 1
template <typename T> void foo(T*); // 2
являются перегрузками, и каждый порождает отдельное семейство возможных специализаций, основой которых они являются.
template <> void foo<int>(int);
это специализация 1, и
template <> void foo<int*>(int*);
это специализация 2.
Чтобы разрешить вызов функции, компилятор сначала выберет лучшую перегрузку, игнорируя при этом специализации шаблона, а затем, если он выбрал функцию шаблона, проверит, имеет ли она какую-либо специализацию, которая могла бы лучше применяться.
3 / Что необходимо в момент создания экземпляра?
Таким образом, из способа, которым компилятор разрешает вызов, мы понимаем, почему в стандарте указывается, что любая специализация должна быть объявлена до его первого этапа создания. В противном случае это просто не будет рассматриваться.
Таким образом, в момент создания экземпляра нужно уже видеть:
- объявление базовой функции шаблона, которая будет использоваться
- декларация о выбранной специализации, если таковая имеется
Но что из определения?
Это не нужно. Компилятор предполагает, что он будет предоставлен позже в TU или другим TU полностью.
Примечание: он обременяет компилятор, потому что это означает, что он должен помнить все неявные экземпляры, с которыми он сталкивался и для которых он не мог выдавать тело функции, так что когда он, наконец, встречает определение, он может (наконец) испустить весь необходимый код для всех специализаций, с которыми он сталкивался. Интересно, почему был выбран именно этот подход, а также удивляюсь, почему даже в отсутствие extern
Объявление TU может заканчиваться неопределенными функциональными телами.
4 / Почему ошибка компоновщика?
Поскольку определения не предоставлено, gcc доверяет вам предоставить его позже и просто отправляет вызов неразрешенному символу. Если вам случится связать с другим TU, который предоставляет этот символ, то все будет хорошо, в противном случае вы получите ошибку компоновщика.
Так как gcc следует за Itanium ABI, мы можем просто посмотреть, как он искажает символы. Оказывается, что ABI не делает различий в искажениях специализаций и неявных реализациях, таким образом
cls.f( asd );
звонки _ZN3cls1fIPKcEEvT_
(который разбирает как void cls::f<char const*>(char const*)
) и специализация:
template<>
void cls::f( const char* )
{
}
также производит _ZN3cls1fIPKcEEvT_
,
Примечание: мне не ясно, могла ли явная специализация дать другое искажение.
Нет, я не думаю, что это нормально
[C++11: 14/6]:
Шаблон функции, функция-член шаблона класса или статический член данных шаблона класса должны быть определены в каждой единице перевода, в которой он неявно создается (14.7.1), если только соответствующая специализация не создается в явном виде (14.7.2) в какой-то переводчик; Диагностика не требуется.
[C++11: 14.7.3/6]:
Если шаблон, шаблон элемента или элемент шаблона класса явно специализированы, то эта специализация должна быть объявлена до первого использования этой специализации, которая вызовет неявную реализацию в каждой единице перевода, в которой такое использование происходит.; Диагностика не требуется. [..]
Честно говоря, я не могу объяснить, почему это работает для вас.
Я думаю, что ваш исходный код был неверным, и ваш "обходной путь" тоже не соответствует стандарту (несмотря на то, что ваш компилятор и компоновщик его обрабатывают). Хорошие цитаты из стандарта были приведены в ответе Lightness Races in Orbit. Смотрите также следующий пример из стандарта ([temp.expl.spec] 14.7.3/6):
class String { };
template<class T> class Array { /* ... */ };
template<class T> void sort(Array<T>& v) { /* ... */ }
void f(Array<String>& v) {
sort(v); // use primary template
// sort(Array<T>&), T is String
}
template<> void sort<String>(Array<String>& v); // error: specialization
// after use of primary template
template<> void sort<>(Array<char*>& v); // OK: sort<char*> not yet used
Я отметил свой ответ как вики сообщества, потому что на самом деле это только большой комментарий.