Проблема в том, что компилятор не пытается использовать предоставленную вами шаблонную operator<<
, а скорее не шаблонную версию.
Когда вы объявляете друга внутри класса, вы внедряете объявление этой функции во вложенную область видимости. Следующий код имеет эффект объявления (и не определения) свободной функции, которая принимает аргумент non_template_test
с помощью константной ссылки:
class non_template_test
{
friend void f( non_template_test const & );
};
// declares here:
// void f( non_template_test const & );
То же самое происходит с шаблонными классами, даже если в этом случае это немного менее интуитивно понятно. Когда вы объявляете (а не определяете) функцию друга в теле класса шаблона, вы объявляете свободную функцию с этими точными аргументами. Обратите внимание, что вы объявляете функцию, а не функцию шаблона:
template<typename T>
class template_test
{
friend void f( template_test<T> const & t );
};
// for each instantiating type T (int, double...) declares:
// void f( template_test<int> const & );
// void f( template_test<double> const & );
int main() {
template_test<int> t1;
template_test<double> t2;
}
Эти свободные функции объявлены, но не определены. Сложность в том, что эти свободные функции не являются шаблоном, а объявляются обычными свободными функциями. Когда вы добавляете функцию шаблона в микс, вы получаете:
template<typename T> class template_test {
friend void f( template_test<T> const & );
};
// when instantiated with int, implicitly declares:
// void f( template_test<int> const & );
template <typename T>
void f( template_test<T> const & x ) {} // 1
int main() {
template_test<int> t1;
f( t1 );
}
Когда компилятор запускает основную функцию, он создает шаблон template_test
с типом int
, который объявляет свободную функцию void f( template_test<int> const & )
, которая не является шаблонной. Когда он находит вызов f( t1 )
, есть два f
символа, которые соответствуют: не шаблон f( template_test<int> const & )
, объявленный (и не определенный), когда был создан экземпляр template_test
, и шаблонная версия, которая объявлена и определена в 1
. Версия без шаблонов имеет приоритет, и компилятор соответствует ей.
Когда компоновщик пытается разрешить не шаблонную версию f
, он не может найти символ и, таким образом, терпит неудачу.
Что мы можем сделать? Есть два разных решения. В первом случае мы заставляем компилятор предоставлять функции без шаблонов для каждого экземпляра типа. Во втором случае мы объявляем шаблонную версию как друга. Они немного отличаются, но в большинстве случаев эквивалентны.
Если компилятор сгенерирует функции без шаблонов для нас:
template <typename T>
class test
{
friend void f( test<T> const & ) {}
};
// implicitly
В результате создается столько бесплатных шаблонных функций, сколько необходимо. Когда компилятор находит объявление друга в шаблоне test
, он не только находит объявление, но также и реализацию и добавляет оба в охватывающую область.
Создание шаблонной версии для друга
Чтобы сделать шаблон другом, мы должны уже объявить его и сообщить компилятору, что наш друг на самом деле является шаблоном, а не свободной от шаблонов свободной функцией:
template <typename T> class test; // forward declare the template class
template <typename T> void f( test<T> const& ); // forward declare the template
template <typename T>
class test {
friend void f<>( test<T> const& ); // declare f<T>( test<T> const &) a friend
};
template <typename T>
void f( test<T> const & ) {}
В этом случае перед объявлением f
в качестве шаблона мы должны предварительно объявить шаблон. Чтобы объявить шаблон f
, мы должны сначала объявить шаблон test
. Объявление друга изменено и теперь включает в себя угловые скобки, которые указывают, что элемент, который мы делаем другом, на самом деле является шаблоном, а не свободной функцией.
Вернуться к проблеме
Возвращаясь к вашему конкретному примеру, самое простое решение состоит в том, чтобы компилятор генерировал функции для вас, вставляя объявление функции friend:
template <typename T>
class BinaryTree {
friend std::ostream& operator<<( std::ostream& o, BinaryTree const & t ) {
t.dump(o);
return o;
}
void dump( std::ostream& o ) const;
};
С помощью этого кода вы заставляете компилятор генерировать не шаблонизированный operator<<
для каждого экземпляра типа, и сгенерированные делегаты функции в методе dump
шаблона.