iostream插入器和提取器可以是类成员而不是全局重载吗?

Could iostream inserters and extractors be class members instead of global overloads?

本文关键字:全局 重载 成员 提取 iostream 插入      更新时间:2023-10-16

必须声明"全局友元操作符重载"才能进行序列化,这总是让我觉得很笨拙。必须在类之外声明序列化操作符似乎并不基本。所以我一直在寻找一个可靠的答案。

(注意:如果有人有更好的Google-Fu找到一个好的答案,我很有兴趣阅读。)

我怀疑这在技术上是可能的,只是一个符号问题。如果库被设计为对<<>>进行成员重载,则必须构建从右到左的流操作行,而不是从左到右。所以我们不写

Rational r (1, 2);
cout << "Your rational number is " << r;

您必须将输出行写成:

r >> ("Your rational number is " >> cout);

需要括号来启动反向链,因为>><<从左到右关联。如果没有它们,它将尝试在"Your rational number is " >> cout之前找到r >> "Your rational number is "的匹配。如果选择了从右到左结合的操作符,则可以避免这种情况:

r >>= "Your rational number is " >>= cout;

(注意:在标准库内部,非类类型,如字符串文字,必须通过全局操作符重载来处理)

但是这是极限吗?对于任何想要将序列化分派到类中的iostream风格设计来说,这种反转几乎是不可避免的吗?还有其他问题吗?


UPDATE也许对这个"问题"更好的说法是,我开始怀疑以下情况:

对于希望序列化自己的非流对象,iostream库可以假设设计为插入器和提取器是类成员而不是全局重载…并且不会(显著地)影响运行时属性。然而,只有当iostream的作者愿意接受它将迫使客户端形成从右向左的流操作时,这才会起作用。

但是,对于为什么全局重载操作符和成员可以解锁从左到右(而不是从右到左)表达自己的能力,我缺乏直觉。问题是后见之明、模板或c++ 11的一些深奥特性是否可以提供替代方案。或者"c++物理"有一个固有的偏向于一个方向而不是另一个方向,而全局重载在某种程度上是书中唯一覆盖它的编译时技巧。

与弗莱明的电机左手定则相比

对于重载流操作符,标准没有限制它们应该是成员还是非成员,所以理想情况下它们可以是成员。实际上,标准库定义的大多数流输出和输入操作符都是流类的成员。

A基本原理:

为什么insertersextractors没有作为成员函数重载?

通常操作符重载的规则是:

如果二元操作符改变了其左操作数,通常将其设置为左操作数类型的成员函数是有用的。(因为它通常需要访问操作数的私有成员)。

根据此规则,流操作符应该作为其左操作数类型的成员来实现。然而,它们的左操作数是来自标准库的流,并且不能更改标准库的流类型。因此,当为自定义类型重载这些操作符时,它们通常被实现为非成员函数。

通过分隔<<而>>来自你正在显示的对象。

首先,您可能希望自定义显示非类类型,如enum或指针。假设你有一个类Foo,你想自定义打印一个指向Foo的指针。不能通过向Foo添加成员函数来实现。或者您可能想要显示一个模板化的对象,但只显示一个特定的模板参数。例如,您可能希望将vector显示为逗号分隔的列表,而将vector显示为string的列。

另一个原因可能是不允许或不愿意修改现有的类。这是OO设计中开放/封闭原则的一个很好的例子,您希望您的类对扩展是开放的,但对修改是封闭的。当然,你的类必须在不破坏封装的情况下公开它的一些实现,但这种情况经常发生(向量暴露它们的元素,复杂暴露Re和Im,字符串暴露c_str,等等)。

你甚至可以定义两个不同的<<对于不同模块中的同一个类,如果有意义的话。

我以前有过同样的问题,看起来插入器和提取器必须是全局的。对我来说,这还可以;我只是想避免在我的类中创建更多的"好友"函数。我是这样做的,即提供一个"<<"可以调用的公共接口:

class Rock {
    private:
        int weight;
        int height;
        int width;
        int length;
    public:
        ostream& output(ostream &os) const {
            os << "w" <<  weight << "hi" << height << "w" <<  width << "leng" << length << endl;
            return os;
        }
    };
    ostream& operator<<(ostream &os, const Rock& rock)  {
        return rock.output(os);
    }

我还遗漏了什么问题吗?

我想不起来,但我得说这已经很糟糕了。

通常的outfile << var1 << var2 << var3;是一种相当"线性"的语法。而且,由于在这两种情况下我们都是从左到右读取,因此名称的顺序将与它们在文件中的顺序相同。

你打算使语法非线性。人类读者将不得不跳到前面,回头看看发生了什么。这让事情变得更加困难。你可以更进一步。要阅读最后一行r >>= "Your rational number is " >>= cout;,您首先必须通过>>=向前阅读,以查看您需要跳到最后一个单词(或附近),阅读">>= cout",跳到字符串的开头,向前阅读字符串,等等。而不是把你的眼睛从一个符号移到另一个符号,在那里大脑能够流水线整个过程。

我有几年使用这种非线性语法的语言的经验。我现在正在考虑使用clang将c++"编译"成那种语言。(不过,原因不止于此。)如果成功了,我会快乐得多。

我更喜欢的替代方法是一个非常小的操作符重载,它只调用成员函数。即使稍微优化一下,它也会从生成的可执行文件中消失。

上面有一个"明显"的例外,那就是在语句中只有一个读/写操作,如myobj >> cout;

在c++中,二进制操作符通常是非类成员。根据Bjarne Stroustrup编写的 c++编程语言操作符,规范表示是一个全局函数,它首先复制左操作数,然后对右操作数使用+=,然后返回结果。因此,让流操作符是全局的并不是什么不寻常的事情。如前所述,我们希望流操作符是流类的成员,而不是数据类的成员。