C++模板友元声明、特化与重载运算符使用相关疑问
版本1代码
#include <iostream> using namespace std; //template<typename T> void func4(); //template<typename T> class NAME; // template<typename T> std::ostream& operator<< (std::ostream&, NAME<T> const&); template<typename T> class NAME { friend void func4<T>(); friend std::ostream& operator<< <T> (std::ostream&, NAME<T> const&); }; int main() { cout << "Hello World" << endl; return 0; }
编译报错信息
prog.cc:13:17: error: variable or field 'func4' declared void 13 | friend void func4<T>(); | ^~~~~ prog.cc:13:17: error: expected ';' at end of member declaration 13 | friend void func4<T>(); | ^~~~~ | ; prog.cc:13:22: error: expected unqualified-id before '<' token 13 | friend void func4<T>(); | ^
问题列表
问题1
我已经将func4和operator<<两个模板函数的前向声明都注释了,为什么只有func4的友元声明报错,operator<<的友元声明没有报错?我清楚如果要友元指定模板的某个实例需要前向声明:即friend void func4<T>();生效需要取消template<typename T> void func4();的注释,同理friend std::ostream& operator<< <T> (std::ostream&, NAME<T> const&);生效需要取消对应前向声明的注释。但当我只取消func4的前向声明注释时,程序可以正常运行,operator<<没有前向声明也不会报错,这又是为什么?
问题2
friend void func4<T>();和friend void func4<>();是否等价?同理friend std::ostream& operator<< <T> (std::ostream&, NAME<T> const&);和friend std::ostream& operator<< <> (std::ostream&, NAME<T> const&);是否等价?我个人认为friend void func4<>();对应函数模板的特化版本,而friend void func4<T>();是显式传入T参数,调用func4<int>();时才会实例化对应版本占用内存;特化版本需要提前提供定义,会提前占用内存。请问使用
问题3
我在cppreference中看到如下描述:
无法为重载运算符、转换函数和构造函数显式指定模板实参,因为调用它们时不会使用函数名。
按照上述描述,重载运算符不能显式指定模板实参,为什么友元声明中可以编写friend std::ostream& operator<< <T> (std::ostream&, NAME<T> const&);这种显式指定模板参数的语法?
解答
问题1解答
差异的核心原因是operator<<不是未声明的标识符,而func4是完全未声明的自定义标识符。<iostream>头文件中已经声明了大量std::operator<<的函数模板,所以即使你注释了自己写的operator<<前向声明,编译器依然能识别operator<<是模板名称,不会报标识符不存在的语法错误,只会在你实际尝试用NAME<T>实例调用operator<<时,才会报无匹配重载的链接/编译错误。而func4是你自定义的函数,没有任何前置声明的情况下,编译器根本不知道func4是个模板,看到func4<T>的写法会直接判定为语法错误。
当你只取消func4的前向声明时,func4已经被识别为模板,operator<<有标准库的模板声明兜底,所以语法层面全部合法,编译可以通过。
问题2解答
两种写法在你的场景下完全等价,你之前的理解是错误的。friend void func4<>();中的空尖括号不是特化标识,是模板实参推导的语法:编译器会根据友元声明的上下文自动推导模板参数,你这个场景下推导出来的参数就是NAME类的模板参数T,和手动写<T>效果完全一致。
对应的两种operator<<的友元声明也完全等价。关于内存占用的结论也不正确:两种写法都是指代对应模板的实例,都是在该实例被ODR使用的时候才会实例化、分配内存,和你用<T>还是<>的写法没有关系,也不存在<>就对应用户特化、需要提前定义的规则。只有当你写template<> void func4<某具体类型>()这种全特化语法的时候,才属于特化声明,和你提到的空尖括号推导场景完全不同。
问题3解答
你误解了这段描述的适用场景:这段话指的是使用运算符语法调用重载运算符的时候,无法显式指定模板实参。比如你不能写出std::cout <<<T> obj这种代码来指定operator<<的模板参数,因为运算符调用的语法里没有位置插入模板实参列表。
而友元声明里你用的是operator<<的函数名形式,不是运算符调用语法,这种场景下不管是声明、定义还是手动函数式调用(比如operator<< <T>(std::cout, obj)),都可以显式指定模板实参,完全符合C++语法规则。
内容的提问来源于stack exchange,提问作者Anoop Rana

