2

Having to declare "global friend operator overloading" to do serialization always struck me as kludgey. It didn't seem foundational to have to declare serialization operators outside of your class. So I was looking for a solid answer for why.

(Note: If anyone has better Google-Fu to find a good answer already written, I'd be interested to read that.)

What I suspect is that it's technically possible, and is only a notational issue. Had the library been designed to do member overloads of << and >>, you'd have to build a line of streaming operations from right to left, instead of left to right. So instead of writing:

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

You would have to write the output line as:

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

The parentheses are needed to kick off the backwards chaining, because >> and << associate left-to-right. Without them, it will try to to find a match for r >> "Your rational number is " before "Your rational number is " >> cout. Had an operator with right-to-left associativity been picked, this could be avoided:

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

(Note: Inside the library, non-class types like the string literal would have to be taken care of with global operator overloads.)

But is that the limit, and this reversal is pretty much inevitable for any iostream-style design that wanted serialization dispatched into the class? Am I missing any other problems?


UPDATE Perhaps a better phrasing of the "issue" is to say that I came to suspect the following:

For non-stream objects that wish to serialize themselves, the iostream library COULD hypothetically have been designed so that inserters and extracters were class members instead of global overloads...and without (significantly) affecting the runtime properties. Yet this would have ONLY worked if the iostream authors were willing to accept it would force clients to form streaming operations right-to-left.

But I lack an intuition about why global overloading an operator vs. a member could unlock an otherwise-unlockable ability to express oneself left-to-right (instead of right-to-left). Question is whether hindsight, or templates, or some esoteric feature of C++11 could offer an alternative. OR does "C++ physics" have an inherent bias for one direction over another, and global overloading is somehow the only compile-time trick in the book for overriding it.

Compare with Fleming's left hand rule for motors

  • Having to read the arguments right-to-left is bad enough. May-be this helps: http://stackoverflow.com/questions/7052568/why-is-the-output-operator-os-value-and-not-value-os/7053589#7053589 :) – UncleBens Oct 22 '11 at 21:36
  • @UncleBens That is an interesting point. Though I was speaking about achieving an equivalent run-time behavior to the existing iostreams while allowing non-stream classes to define >> and << (or I'd accept other operators) as member functions instead of global friends. Your technique means one pays the price at run-time...which is a fit sometimes, but unsuitable for the design. BTW if this kind of thought experiment interests you, you might like "analog literals" http://hostilefork.com/2009/08/29/tweakinganalog-literals-humor/ – HostileFork says dont trust SE Oct 23 '11 at 14:03
  • I assumed that you wanted operator >> to be a member of the object you're displaying, but some of the discussion points at a different interpretation: >> being a member of the stream. Which is it? – Bartosz Milewski Oct 23 '11 at 17:22
  • @BartoszMilewski Yep, it got pointed out that << and >> could be member functions IF they were inside of the stream...and technically that addressed what I initially asked, at least as far as the question's title went. But I tried to clarify via comments and an update to the question ("...non-stream objects that wish to serialize themselves...") – HostileFork says dont trust SE Oct 23 '11 at 19:31

5 Answers5

3

For overloading stream operators there is no restriction imposed by the Standard on whether they should be members or non-members, So Ideally they can be.In fact, most of the stream output and input operators defined by the standard library are members of the stream classes.

A Rationale:

Why inserters and extractors are not overloaded as member functions?

Usually the rule for operator overloading is:

If a binary operator changes its left operand it is usually useful to make it a member function of its left operand’s type.(Since it will usually need to access the operands private members).

By this rule Stream operators, should be implemented as members of their left operand’s type. However, their left operands are streams from the standard library, And One cannot change the standard library’s stream types. Hence these when overloading these operators for custom types, they are usually implemented as non-member functions.

Alok Save
  • 202,538
  • 53
  • 430
  • 533
  • I think the question was not about things are done, but why the library hadn't been designed differently, so that the stream is the right-hand operand, and getting goodness like `"C" >> "B" >> "A" >> cout;` outputting "ABC". – UncleBens Oct 22 '11 at 21:40
  • @UncleBens Actually I don't *want* the reversal. The only thing I was looking at was how to make the stream operators members of a non-stream class that wants to be able to serialize itself. I was just seeking the foundational reason why C++ couldn't do that--even if one were willing to use other operators or tricks...wondering if any loophole would let one keep it left to right. – HostileFork says dont trust SE Oct 23 '11 at 01:00
  • @als I understand what you're saying, and that implementing one's own stream classes means you can do your operators as class members...of the *stream*. (I was only thinking about objects that weren't streams but wanted to be serialized to them.) I've updated the question to hopefully better capture the language design curiosity I'm trying to get across... – HostileFork says dont trust SE Oct 23 '11 at 15:04
  • @HostileFork:Ah, In that case this answer is pretty much useless to answer your real Question, it just answers the meta Q in title. – Alok Save Oct 24 '11 at 04:33
2

I had the same question before, and looks like inserters and extracters have to be global. To me, it is ok; I just want to avoid making more "friend" functions in my class. This is how I do it, i.e., provid a public interface that "<<" can call:

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);
    }
Haoru HE
  • 21
  • 1
2

You get more flexibility by separating the definition of << and >> from the object you're displaying.

First of all, you might want to custom-display a non-class type, like an enum, or a pointer. Suppose you have a class Foo and you want to custom-print a pointer to Foo. You can't do it by adding a member function to Foo. Or you might want to display a templated object but only for a particular template parameter. For instance, you might want to display a vector<int> as a comma separated list, but a vector<string> as a column of string.

Another reason might be that you're not allowed or willing to modify an existing class. It's a good example of the open/closed principle in OO design, where you want your classes to be open for extension but closed for modification. Of course, your class has to expose some of its implementation without breaking encapsulation, but that's often the case (vectors expose their elements, complex exposes Re and Im, string exposes c_str, etc.).

You may even define two different overloads of << for the same class in different modules, if it makes sense.

Bartosz Milewski
  • 11,012
  • 5
  • 36
  • 45
  • Good point about the decoupling...and that perhaps if you have to "friend" your serialization operations you haven't exposed enough of an API (that people using the class might want for reasons other than I/O...) – HostileFork says dont trust SE Oct 25 '11 at 13:30
1

Am I missing any other problems?

Not that I can think of, but I'd say that's pretty bad already.

The usual outfile << var1 << var2 << var3; is a fairly "linear" syntax. And, since we read left-to-right in both cases, the names will be in the same order as they will be in the file.

You're planning to make the syntax non-linear. A human reader will have to skip ahead and come back to see what's happening. This makes it harder. And you go even further. To read your last line, r >>= "Your rational number is " >>= cout; you first have to read forward through the >>= to see that you need to skip to the last word (or thereabouts), read ">>= cout", skip back to the beginning of the string, read forward through the string, and so on. As opposed to moving your eye(s) from one token to the next, where the brain is able to pipeline the whole process.

I have several years of experience with a language with such a non-linear syntax. I'm now looking into using clang to "compile" C++ into that language instead. (For more reasons than that, though.) If it works out, I'll be far happier.

My preferred alternative is a very minimal operator overload that only calls a member function. If optimizing even slightly, it will disappear from the produced executable anyway.

There's one "obvious" exception to the above, which is when you only have a single read/write in a statement, as in myobj >> cout;

MaHuJa
  • 3,148
  • 1
  • 18
  • 6
  • You could easily make the stream (or something) store up the values it's given, then print them out in reverse, in order to avoid the `r >>= "Here's a value: " >>= cout` thing. Of course, I think you'd need something at the beginning of the chain to trigger a flush. – Chris Lutz Oct 22 '11 at 23:23
  • To read them in reverse, you must tell it the entire sequence you're about to read. Not just how many elements, but what types. That solution would be quite verbose and completely defeat the purpose of this. – MaHuJa Oct 22 '11 at 23:57
  • Why would you have to specify everything? You could just add `special_object >>= stuff >>= to >>= print >>= cout` and have `special_object` be of a type that tells the stream to flush. You can convert all the objects to strings with a `strstream` and store them all in a `vector` (or just a `string`, and prepend each new addition) before you read the `special_object` that flushes them all. – Chris Lutz Oct 23 '11 at 00:31
  • Despite the code sample being about output, I was thinking in terms of input when I wrote that. So, you're right you can do that for output. The question is however for both input and output. – MaHuJa Oct 23 '11 at 01:57
  • Input would be much trickier. I really want to spend four hours trying to work out how to do it, but I have a paper to write so it'll have to wait. – Chris Lutz Oct 23 '11 at 02:17
  • *"You're planning to make the syntax non-linear."* ... Well, I wasn't exactly making a formal proposal for the C++2X standard :) I was just trying to get to the core of why C++ was fundamentally incapable of having a just-as-good syntax for streaming, without compromising any runtime aspect, and yet let stream operators be members of classes that wanted to serialize. What I'm proposing is basically that there's no compile-time-only evaluation trick to do this besides writing things right-to-left, which is functionally equivalent but notationally more confusing...! – HostileFork says dont trust SE Oct 23 '11 at 13:58
  • @MaHuJa Good mention of the "minimal overload" that calls a method rather than having to mess with `friend`. I've *wanted* to do that, but my main hangup is inventing names for those methods and potentially confusing readers accustomed to the conventional idiom... – HostileFork says dont trust SE Oct 23 '11 at 14:08
  • @HostileFork I have a suggestion: `istream& foo::operator<< (istream&)` which would separately allow `f << cin` syntax for input. (Not that it'll handle chaining well.) Then, the global `istream& operator>> (istream& i, foo& f) { f << i; }` would allow for the usual usage. All said, though, Bartosz' answer does give some useful flexibility. – MaHuJa Nov 13 '11 at 10:28
1

In C++ binary operators are generally non-class members. According to The C++ Programming Language by Bjarne Stroustrup operator+ the canonical representation is a global function that first copies its left operand and then uses += onto it with the right operand, and then returns the result. So having the stream operators be global is not at all out of the ordinary. As Als mentioned, we would expect the stream operators to be members of the stream classes rather than of the data classes.

Tom
  • 93
  • 1
  • 6