Ideally all functions should return just one value. There are many times, however, when returning more than one value makes a function so much more convenient. The classic example of this convenience is file input. When we read data from a file we want to know two things: Did we reach the end of the file? and if not, what is the next piece of data from the file.
Sometimes, we can encode multiple return values into one. For many of us, the first C idiom we learned from K&R is processing input a character at a time:
int ch;while ((ch = getchar()) != EOF) { // do character processing...}
This works because the EOF macro was set to something outside the range of valid characters (usually -1). While this approach can work fairly well for simple cases, it quickly breaks down as the types we wish to return get more complex.
One way to return multiple values is to return one value through the function return mechanism and to pass pointers or references to objects which will receive the additional parameters as additional arguments to the function. For the sake of example, we are going to create a simple function which returns a single line of text from an input stream. This function will take one input parameter, the input stream, and return two values. The first value will be a boolean to indicate success and the second value will be the string that was read. Using the approach just described, we could code it up as:
bool readline(istream& is, string& line){ if (is.eof()) return false; getline(is, line); return true;}
And we would call it like this:
string line;while (readline(ss, line)) cout << line << endl;
So, what’s wrong with this approach?
There’s a couple of issues.
Another approach is to return a struct which contains all the return values. Our line reading function now becomes:
struct line_ret{ bool success; string line;};line_ret readline(istream& is){ line_ret lr = { false, "" }; if (!is.eof()) { lr.success = true; getline(is, lr.line); } return lr;}
And we would call it with:
for (;;) { line_ret lr = readline(ss); if (!lr.success) break; cout << lr.line << endl;}
At first this seems like a reasonable approach. The problem here is that we needed to define a struct whose only use is to represent the return values from this function. Structs should be used to hold logically related items. A good rule of thumb is if a struct (or class, obviously) doesn’t already exist, don’t create it just to contain multiple return values.
The last approach (and the whole point of this post) is to use tuples. I am going to demonstrate the same example using the Boost.Tuple library but, if your compiler supports it, you could just as easily use TR1 tuples. They are the same thing. Using tuples, our code now looks like:
#include <boost/tuple/tuple.hpp>using namespace boost;tuple<bool,string> readline(istream& is){ if (is.eof()) return tuple<bool,string>(false); string line; getline(is, line); return tuple<bool,string>(true, line);}
This version returns a tuple which consists of a bool an a string. Note that the failure case has only the boolean in the return statement. Since the string is not provided, the default string constructor will be called to create an empty string which will be returned. We could call this function like this:
for (;;) { tuple<bool,string> ret = readline(ss); if (!ret.get<0>()) break; cout << ret.get<1>() << endl;}
The get<n>() syntax returns the nth member of a tuple. While this works, it’s not very clear. Fortunately, you can use a tier (pronounce it “tire” instead of “tear” and the name makes much more sense) to help out with that:
for (;;) { bool success; string line; tie(success, line) = readline(ss); if (!success) break; cout << line << endl;}
The tier ties success with the first tuple item and line with the second tuple item. That makes the resulting code much easier to read and we get compile-time checking for all the values we’re returning.
Hi Ferruccio,
2 comments:
1) There is an error in all versions of readline() in your post: Testing a stream for eof() is not a guarantee of success. The stream could be in another fail state. Checking the return value of getline would be safer (and more idiomatic). In your tuple-returning version, that would give something like:
string line;bool success = getline(is, line);return make_tuple(success, line);
2) The Boost.Optional (http://www.boost.org/doc/libs/1_37_0/libs/optional/doc/html/index.html) library does something very similar, with added syntactic sugar:
Definition of readline:
optional<string> readline(istream& is){ optionall<string> result; string line; if (getline(is, line)){ result = line; } return result; }
Use:
while(optionall<string> s = readline(ss)){ cout l<l< s l<l< endl;}
(Note: none of the code in this comment was tested, or even compiled. It is only OTOMH)
Comment by éric Malenfant — January 6, 2009 @ 6:14 pm
éric,
Yes, of course, you’re right. This was a trivial example designed to show how to return multiple values. A more realistic example might be a function that returns a couple of types not expressible by using boost.optional (or returned three value), but I wanted to keep the example simple.
Thanks for the reminder about optional. That is a very handy way to have nullable types in C++.
– Ferruccio
Comment by Ferruccio — January 6, 2009 @ 7:54 pm
Hi.
First of all, nice blog!
I like tuples but in this case I think a simple STL pair suffices.
About getchar() and similar functions.
Steve Maguire in his old “Writing Solid Code” book is pretty explicit: they should return just a “flag” (boolean) while putting the result in a pointer-type argument allocated by the caller.
This should make also the function a bit more reentrant.
jp
联系客服