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

时间:2011-10-22 19:34:04

标签: c++ operator-overloading language-design iostream associativity

必须声明“全局朋友操作员重载”来进行序列化总是让我感到震惊。在类之外声明序列化运算符似乎没有基础。所以我一直在寻找一个可靠的答案。

(注意:如果有人有更好的Google-Fu来找到已经写好的答案,我有兴趣阅读。)

我怀疑它在技术上是可行的,而且只是一个符号问题。如果该库被设计为执行<<>>的成员重载,则必须从右到左构建一系列流操作,而不是从左到右。所以不要写:

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

您必须将输出行写为:

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

需要使用括号来启动向后链接,因为>><<从左到右关联。没有它们,它会尝试在r >> "Your rational number is "之前找到"Your rational number is " >> cout的匹配项。如果选择了right-to-left associativity的运营商,则可以避免这种情况:

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

(注意:在库内部,像字符串文字这样的非类类型必须使用全局运算符重载来处理。)

但这是极限,对于任何想要将序列化分派到课堂中的iostream式设计来说,这种逆转几乎是不可避免的吗?我错过了其他任何问题吗?


更新或许对“问题”的更好的措辞是说我开始怀疑以下内容:

对于希望自行序列化的非流对象,假设iostream库可以设计为插入器和extracters是类成员而不是全局重载......并且没有(显着)影响运行时属性。然而,如果iostream作者愿意接受它会迫使客户从右到左形成流媒体操作,那么这只会有效。

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

Fleming's left hand rule for motors

比较

5 个答案:

答案 0 :(得分:3)

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

基本原理:

为什么insertersextractors不会作为成员函数重载?

通常,运算符重载的规则是:

如果二元运算符改变了它的左操作数,那么使它成为左操作数类型的成员函数通常很有用。(因为它通常需要访问操作数私有成员)。

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

答案 1 :(得分:2)

通过分离&lt;&lt;的定义,您可以获得更大的灵活性和&gt;&gt;来自您正在显示的对象。

首先,您可能希望自定义显示非类类型,如枚举或指针。假设您有一个类Foo,并且您想要自定义打印指向Foo的指针。你不能通过向Foo添加成员函数来实现。或者您可能希望显示模板化对象,但仅适用于特定模板参数。例如,您可能希望显示向量&lt; int&gt;以逗号分隔的列表,但向量&lt; string&gt;作为一列字符串。

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

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

答案 2 :(得分:2)

之前我遇到过同样的问题,看起来插件和extracters必须是全局的。对我来说,没关系;我只想避免在班上制作更多“朋友”的功能。 这就是我这样做的方式,即提供一个“&lt;&lt;”可以打电话:

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);
    }

答案 3 :(得分:1)

  

我错过了其他任何问题吗?

不是我能想到的,但我已经说过这已经很糟糕了。

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

您计划将语法设为非线性。人类读者必须跳过并回过头来看看发生了什么。这使得它变得更难。你走得更远。要阅读你的最后一行r >>= "Your rational number is " >>= cout;,首先必须通过&gt;&gt; =向前阅读,看看你需要跳到最后一个单词(或左右),阅读“&gt;&gt; = cout”,跳回字符串的开头,通过字符串向前读取,依此类推。而不是将你的眼睛从一个标记移动到下一个标记,在那里大脑能够管理整个过程。

我对使用这种非线性语法的语言有多年的经验。我现在正在研究使用clang将C ++“编译”成该语言。 (尽管有更多的原因,但是。)如果它成功了,我会更快乐。

我首选的替代方案是一个非常小的运算符重载,它只调用一个成员函数。如果稍微优化,它将从生成的可执行文件中消失。

上面有一个“显而易见”的例外,即当你在一个语句中只有一个读/写时,就像myobj >> cout;

一样

答案 4 :(得分:1)

在C ++中,二元运算符通常是非类成员。根据Bjarne Stroustrup运算符的 C ++编程语言,规范表示是一个全局函数,首先复制其左操作数,然后使用右操作数对其使用+ =,然后返回结果。因此让流运营商成为全球运营商并非完全不同寻常。正如Als所提到的,我们希望流操作符是流类的成员而不是数据类的成员。