如何正确地为 ostream 重载 << 运算符?

283
我正在用C++编写一个用于矩阵操作的小型矩阵库。然而,我的编译器报错了,而之前却没有。这段代码已经放在书架上六个月了,在此期间我将电脑从Debian 4.0(Etch)升级到了Debian 5.0(Lenny)(g++(Debian 4.3.2-1.1)4.3.2)。然而,在一个使用相同g++的Ubuntu系统上,我遇到了同样的问题。
这是我矩阵类的相关部分:
namespace Math
{
    class Matrix
    {
    public:

        [...]

        friend std::ostream& operator<< (std::ostream& stream, const Matrix& matrix);
    }
}

而且还有“实施”:
using namespace Math;

std::ostream& Matrix::operator <<(std::ostream& stream, const Matrix& matrix) {

    [...]

}

这是编译器给出的错误信息:
matrix.cpp:459: 错误:'std::ostream& Math::Matrix::operator<<(std::ostream&, const Math::Matrix&)' 必须只接受一个参数
对于这个错误我有点困惑,但是在过去的六个月里,我一直在做很多Java,所以我的C++有点生疏了。 :-)
6个回答

157
我只是告诉你另一种可能性:我喜欢使用朋友定义来解释这个。
namespace Math
{
    class Matrix
    {
    public:

        [...]

        friend std::ostream& operator<< (std::ostream& stream, const Matrix& matrix) {
            [...]
        }
    };
}

该函数将自动定位到周围的命名空间Math(即使其定义出现在该类的范围内),但除非您使用Matrix对象调用operator<<,否则不可见,这将使参数相关查找找到该运算符的定义。这有时可以帮助解决模糊的调用,因为对于Matrix以外的参数类型,它是不可见的。在编写其定义时,您还可以直接引用Matrix中定义的名称以及Matrix本身,而无需使用一些可能很长的前缀并提供模板参数,例如Math::Matrix<TypeA, N>

145
你已将函数声明为“friend”。它不是类的成员。你应该从实现中删除“Matrix ::”。"friend" 意味着指定的函数(不是类的成员)可以访问私有成员变量。你实现函数的方式就像一个错误的 Matrix 类实例方法。

9
你还需要在 Math 命名空间内声明它(不仅仅是使用 using namespace Math)。 - David Rodríguez - dribeas
1
为什么operator<<必须在Math的命名空间中?它似乎应该在全局命名空间中。我同意我的编译器希望它在Math的命名空间中,但这对我来说没有意义。 - Mark Lakata
抱歉,我不明白为什么我们在这里使用friend关键字。当在类中声明友元运算符重载时,似乎我们无法使用Matrix::operator<<(ostream& os, const Matrix& m)进行实现。相反,我们需要只使用全局运算符重载operator<<ostream& os, const Matrix& m),那么为什么要在类内部声明它呢? - Patrick
Patrick,你可以使用 friend 关键字来让实现代码能够访问私有(和受保护的)成员变量。例如,你可能想要打印矩阵的所有元素,而这些元素可能是私有的。 - Erik

93
补充Mehrdad的回答
namespace Math
{
    class Matrix
    {
       public:

       [...]


    }
    std::ostream& operator<< (std::ostream& stream, const Math::Matrix& matrix);
}

在你的实施中
std::ostream& operator<<(std::ostream& stream,
                     const Math::Matrix& matrix) {
    matrix.print(stream); // Assuming you define print for matrix
    return stream;
 }

4
我不明白为什么会被踩,这表明你可以将运算符声明为命名空间的一部分,甚至不需要声明为友元,还说明了如何可能声明该运算符。 - kal
2
Mehrdad的回答中没有任何代码片段,所以我只是添加了可能起作用的代码,将其移动到命名空间本身之外。 - kal
我理解您的观点,我只看了您的第二个片段。但现在我看到您将运算符从类中取出。感谢您的建议。 - Matthias van der Vlies
7
它不仅超出了类的范畴,而且是在Math名称空间内被定义的。此外,它有一个额外的优点(可能对于Matrix类没有用),即'print'可以是虚拟的,因此打印将在继承的最终级别上发生。 - David Rodríguez - dribeas

80

假设我们正在讨论为所有派生自std::ostream的类重载operator <<以处理Matrix类(而不是重载<<以处理Matrix类),在头文件中将重载函数声明到Math命名空间之外更有意义。

只有当通过公共接口无法实现功能时,才使用友元函数。

Matrix.h

namespace Math { 
    class Matrix { 
        //...
    };  
}
std::ostream& operator<<(std::ostream&, const Math::Matrix&);

注意运算符重载是在命名空间之外声明的。

Matrix.cpp

using namespace Math;
using namespace std;

ostream& operator<< (ostream& os, const Matrix& obj) {
    os << obj.getXYZ() << obj.getABC() << '\n';
    return os;
}

另一方面,如果你的重载函数确实需要成为友元函数,即需要访问私有和保护成员。

Math.h

namespace Math {
    class Matrix {
        public:
            friend std::ostream& operator<<(std::ostream&, const Matrix&);
    };
}

您需要使用命名空间块括起函数定义,而不仅仅是使用using namespace Math;

Matrix.cpp

using namespace Math;
using namespace std;

namespace Math {
    ostream& operator<<(ostream& os, const Matrix& obj) {
        os << obj.XYZ << obj.ABC << '\n';
        return os;
    }                 
}

1
这里只是有点挑剔..我认为os在这种情况下是一个很差的缩写(它太过于与“操作系统”紧密相关)。 - LeonTheProfessional
operator<< 重载能否在类内部进行声明和实现,而不需要使用友元? - KcFnMi

49
在C++14中,您可以使用以下模板来打印任何具有T::print(std::ostream&)const成员函数的对象。
template<class T>
auto operator<<(std::ostream& os, T const & t) -> decltype(t.print(os), os)
{
    t.print(os);
    return os;
}

在C++20中,可以使用概念。
template<typename T>
concept Printable = requires(std::ostream& os, T const & t) {
    { t.print(os) };
};

template<Printable T>
std::ostream& operator<<(std::ostream& os, const T& t) {
    t.print(os);
    return os;
}

有趣的解决方案!一个问题 - 这个运算符应该在哪里声明,比如全局范围内?我认为它应该对所有可以用于模板化的类型可见。 - barney
@barney 它可以与使用它的类一起在您自己的命名空间中。 - QuentinUK
既然返回类型是 std::ostream&,你不能直接返回它吗? - Jean-Michaël Celerier
7
decltype 确保只有在 t::print 存在时才使用该运算符,否则它将尝试编译函数体并给出编译错误。 - QuentinUK
概念版本已添加,在此处进行了测试 https://godbolt.org/z/u9fGbK - QuentinUK

4

我想通过一个示例来简化一下,这个示例是将<<重载以打印数组。

  1. 首先在<<运算符周围传递对象类型
  2. 创建以下函数来重载运算符。
#include<iostream> 
using namespace std;

void operator<<(ostream& os, int arr[]) {
    for (int i = 0;i < 10;i++) {
        os << arr[i] << " ";
    }
    os << endl; 
}
    
int main() {
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    cout << arr;
}

如果需要级联操作, 确保在重载函数中将cout对象返回如下:
#include<iostream> 
using namespace std;

ostream& operator<<(ostream& os, int arr[]) {
    for (int i = 0;i < 10;i++) {
        cout << arr[i] << " ";
    }
    cout << endl; 
    return os;
}
    
int main() {
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    int arr2[10] = { 11,22,33,44,55,66,77,88,99,100 };
    // cascading of operators
    cout << arr << arr2;
}

1
你可以让它适用于任何大小的数组:- template<int N> ostream& operator<<(ostream& os, int(& arr)[N]) { etc - QuentinUK
2
operator==函数内部,应该使用os而不是cout吗? - mcleod_ideafix

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接