Безопасно ли размещать определение специализации функции-члена шаблона (без тела по умолчанию) в исходном файле?

Вот что я имею в виду:

// 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. Какой смысл в создании экземпляра?
  2. Как компилятор выбирает специализацию?
  3. Что необходимо в момент создания экземпляра?
  4. Почему ошибка компоновщика?

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

Я отметил свой ответ как вики сообщества, потому что на самом деле это только большой комментарий.

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