代码之家  ›  专栏  ›  技术社区  ›  Sumudu Fernando

模板类的模板友元函数

  •  9
  • Sumudu Fernando  · 技术社区  · 15 年前

    我正在努力解决中描述的问题 this question (将模板函数声明为模板类的朋友),我认为第二个答案就是我想要做的(向前声明模板函数,然后将专门化命名为朋友)。我有一个问题,一个稍微不同的解决方案究竟是正确的还是恰好在Visual C++ 2008中工作。

    测试代码为:

    #include <iostream>
    
    // forward declarations
    template <typename T>
    class test;
    
    template <typename T>
    std::ostream& operator<<(std::ostream &out, const test<T> &t);
    
    template <typename T>
    class test {
      friend std::ostream& operator<< <T>(std::ostream &out, const test<T> &t);
      // alternative friend declaration
      // template <typename U>
      // friend std::ostream& operator<<(std::ostream &out, const test<T> &t);
    
      // rest of class
      };
    
    template <typename T>
    std::ostream& operator<<(std::ostream &out, const test<T> &t) {
      // output function defined here
      }
    

    首先,我发现一件奇怪的事情是,如果我更改了 operator<< 使其不匹配(例如, std::ostream& operator<<(std::ostream &out, int fake); ,所有东西仍然编译并正常工作(为了清楚起见,我不需要定义这样的函数,只声明它)。但是,正如在“链接到问题”中一样,删除forward声明会导致一个问题,因为编译器似乎认为我声明的是数据成员而不是友元函数。我敢肯定,这种行为是Visual C++ 2008的bug。

    有趣的是,当我移除正向声明并在上面的代码中使用替代友元声明时。注意模板参数 U 不出现在以下签名中。此方法也可以正确编译和工作(不更改任何其他内容)。我的问题是这是否符合Visual C++ 2008的标准或特质(我在参考书中找不到一个好答案)。

    注意,当一个朋友声明 template <typename U> friend ... const test<U> &t); 同样有效,这实际上提供了运算符的每个实例 friend 访问的任何实例 test 我想要的是 test<T> 只能从 operator<< <T> . 我通过实例化 test<int> 操作员<< 以及访问私有成员;当我尝试输出 test<double> .

    概要:删除前面的声明并切换到上面代码中的替代朋友声明似乎产生相同的结果(在Visual C++ 2008中)-这个代码实际上是正确的吗?

    更新:以上代码中的任何修改在GCC下都不起作用,所以我猜想Visual C++编译器中的这些错误或“特性”。不过,我还是很欣赏熟悉该标准的人的见解。

    1 回复  |  直到 15 年前
        1
  •  7
  •   Community CDub    7 年前

    …如果我更改了运算符的前向声明,使其不匹配

    friend函数应该被视为一种非常特殊的声明类型。本质上,编译器足够分析声明,但是除非您真正专门化了类,否则不会进行语义检查。

    在进行建议的修改之后,如果您随后实例化 test 您将得到一个声明不匹配的错误:

    template class test<int>;
    

    然而……删除转发声明会导致问题

    编译器试图解析声明来存储它,直到类模板被专门化。在分析过程中,编译器到达 < 在声明中:

    friend std::ostream& operator<<  <
    

    唯一的方法就是 operator<< 后面可能是 < 如果它是模板,则进行查找以检查它是否是模板。如果找到函数模板,则 < 被认为是模板参数的开始。

    删除转发声明时,找不到模板,并且 操作员<< 被认为是一个物体。(这也是为什么当你添加 using namespace std 代码继续编译,因为必须存在模板声明 操作员<< )

    …删除转发声明并使用上面代码中的替代友元声明时。请注意,模板参数u没有出现在以下签名中…

    不要求在函数模板的参数中使用所有模板参数。另一种声明用于新的函数模板,只有在命名空间中声明并指定显式模板参数时,才能调用该模板。

    一个简单的例子是:

    class A {};
    template <typename T> A & operator<<(A &, int);
    
    void foo () {
      A a;
      operator<< <int> (a, 10);
    }
    

    …此代码是否正确?…

    好吧,这有两个部分。第一个问题是,替代friend函数在后面的作用域中不引用声明:

    template <typename T>
    class test {
      template <typename U> 
      friend std::ostream& operator<<(std::ostream &out, const test<T> &t);
      };
    
    template <typename T> 
    std::ostream& operator<<(std::ostream &out, const test<T> &t);  // NOT FRIEND!
    

    对于每个专门化,friend函数实际上将在命名空间中声明:

    template <typename U> 
    std::ostream& operator<<(std::ostream &out, const test<int> &t);
    template <typename U> 
    std::ostream& operator<<(std::ostream &out, const test<char> &t);
    template <typename U>
    std::ostream& operator<<(std::ostream &out, const test<float> &t);
    

    每一个专业 operator<< <U> 将根据其参数的类型访问特定的专门化 test<T> . 因此,本质上,访问是根据您的需要进行限制的。但是,正如我前面提到的,这些函数基本上不能用作运算符,因为您必须使用函数调用语法:

    int main ()
    {
      test<int> t;
      operator<< <int> (std << cout, t);
      operator<< <float> (std << cout, t);
      operator<< <char> (std << cout, t);
    }
    

    根据前一个问题的答案,您可以使用以下建议的转发声明: litb ,或者您按照 Dr_Asik's 回答(我可能会这么做)。

    更新:第二条评论

    …在类之前更改前向声明;类中的声明仍然与我稍后实现的函数匹配…

    正如我上面指出的,编译器检查 操作员<< 当它看到 < 在声明中:

    friend std::ostream&operator<<<
    

    它通过查找名称并检查它是否是模板来实现这一点。只要您有一个虚拟的转发声明,那么这个“技巧”编译器将您的朋友视为模板名,因此 < 被认为是模板参数列表的开头。

    稍后,当您实例化类时,您确实有一个有效的模板来匹配。本质上,您只是在欺骗编译器,让它将朋友视为模板专门化。

    您可以在这里这样做,因为(如我前面所说),此时不会进行语义检查。