MYF

[RA] Ch16 Strings and IO Revisited

Reading Assignment: All of Programming Chapter 16 Strings and IO Revisited

Strings

C++ provides a string class(std::string for which you need to #include <string>). The string class encapsulates the sequence of bytes which form a string into an object that provides a variety of methods and operators which operate on that type.

For full reference on C++’s string, consult: http://www.cplusplus.com/reference/string/string/

Cautionary Example

1
2
3
4
5
6
7
8
9
#include<string>
// This code is terribly inefficient
std::string reverse(std::string s, std::string ans){
if(s.length() == 0) return ans;
return reverse(s.substr(1), s[0] + ans);
}
std::string reverse(std::string s){
return reverse(s, "");
}

This function is not actually tail recursive, so the complier can not apply tail call optimization to it. The frames for reserve have four string objects(s, ans, as well as temporary objects for s.substr(1) and s[0] + ans), which have non-trivial destructors.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <string>
// This code is much more efficient!
std::string reverse(const std::string &s, std::string &ans, size_t ind){
if(ind == s.length()){
return ans;
}
ans[ind] = s[s.length() - ind - 1];
return reverse(s, ans, ind + 1);
}
// Note that the next function is not recursive.
// It calls the other reverse function, not it self.
std::string reverse(std::string s){
std::string ans(s.length(), '\0');
return reverse(s, ans, 0);
}

This more efficient implementation is about 2000x faster when reversing large strings.

We can also implemente the reverse function iteratively, which has indistinguishable performance from the efficient recursive implementation:

1
2
3
4
5
6
7
8
9
#include <string>

std::string reverse(std::string s){
std::string ans(s.length(), '\0');
for(size_t i = 0; i < s.length(); i++){
ans[i] = s[s.length() - 1 - i];
}
return ans;
}

We present this cautionary tale not for the specific details of implementing string reverse, but to underscore the importance of understanding the implication of all of the code you write. In C++, it is very easy to write code that copies value excessively, and creates/destroy many temporary objects. Such code can be painfully slow.

Output

In C++, the fundamental type for output operations is a std::ostream, and the << operator is overloaded to work with it as the left-hand operand, with a variety of possible types for its right-hand operand. When using the << operator to work with std::ostreams, it is called the stream insertion operator, otherwise it is typically called the left shift operator, as it performs bit shift left for integers.

Instead of using stdout, C++ introduces std::cout which is a std::ostream(likewise, there is std::cerr, which is analogous to stderr).

These classes and functions are included in iostream library.

Return Type of Stream Insertion

How does it work to have multiple <<s in a single statement?

After we write the data to the stream, we then result of the operator, which is what it evaluates to in the expression in which it was used.

This convention is important to remember if you overload the << for your own types. If you make the overloaded operator return the ostream & which was passed in as its left operand, your << will be usable in the normal way-that is, you can chain multiple of your << operators, and/or mix them with other overloadings in a single statement. If you make your << return void, you will not be able to chain them together.

Writing Stream Insertion for Your Own Classes

We may wish for our printing method to have access to fields and methods of our class which are not exposed in the public interface. C++ resolves this conundrum by allowing a class to declare functions or class as its friend. When a class declares a function or a class as its friend, that function or class is allowed access to the private elements of the declaring class. The friend declaration is placed inside of the class wishing to grant access to its private members:

1
2
3
class MyClass{
friend std::ostream & operator << (std::ostream & stream, const MyClass & data);
}

Observe that for functions, we declare the friendship by writing the entire prototype of the function. As overloaded functions are different from each other, a function with the same name, but different parameter types as a friend, would not itself be a friend(unless also declared as such explicitly).

Using friend for operators which are overloaded that are, in principle, part of the class due to the ordering of the parameters is a legitimate use of friend. Except for this, you can probably avoid the use of friend in any C++ program you write, and not have any problems.

Object Orientation of This Approach

C++ adopts this approach as it presents a more object-oriented way to handle IO operation. By using objects, the underlying IO functionality is encapsulated into a class-the << and >> operator ask the stream object to perform output and input respectively with their right operands as arguments.

Controlling Formatting

The stream has internal state that keeps track of the current formatting specifications.

To change the base in which integers are printed, the first approach is used, using std::hex, std::dec, or std::oct accordingly. For example, std::cout << std::hex << x << "\n";. We note that it is generally good practice to put a stream back into decimal mode when you are done printing things in other modes.

Other types of formatting are controlled via methods in the stream class. std::cout.width(5); would set the field width of cout to 5, meaning that output conversion which use field width will print at least 5 characters. What extra characters get printed is controlled in the desired characters. Similarly, the floating point precision can be controlled with the precision method.

Input

We can use stream objects istreams to perform input. Just like for ouput streams and the stream insertion operator, you can overload the stream extraction operator for input streams and classes that you write. You should write your stream extraction operator to take a reference to the input stream.

1
2
3
4
std::istream & operator >> (std::istream & stream, MyClass & data){
// whatever it takes to read things.
return stream;
}

Errors

The input stream object contains an error state internally. When an operation produces an error, it sets the stream’s error state. This error state persists, causing future operation to fail, until it is explicitly cleared. When an error occurs, the input which could not be converted is left unread, so future read operations(after the error state is cleared).

1
2
3
4
5
6
7
8
9
10
11
12
13
int x;
std::cin >> x;
if(!std::cin.good()){ // check if any errors happened
std::cin.clear(); // clear the error state
std::string badinput;
std::cin >> badinput; // read a string
if(!std::cin.good()){
std::cerr << "Cannot read anything from cin!\n";
}
else{
std::cerr<< "Oops! " << badinput << "wasn't an int!\n";
}
}

In this example, we check std::cin.good(). If an error has happened, we use std::cin.clear() to clear the error state so that subsequent operations can succeed.

C++’s input stream acctually distinguish between three types of failure: end of file(which can be checked with .eof()), logical errors on the operation-such as an inability to perform the proper conversion(which can be checked with .fail()), and errors with the underlying I/O operation(which can be checked with .bad() if .fail() returned false).

It is important to know that if you use >> operator to read a string, it will only read one word, not one line. If you want to read an entire line, use std::getline function which takes an input stream reference, and a string reference, and fills in the string with the next line of input from the stream. Note that std::getline is not a member of any class, it is just a function.

Other Stream

Like C, C++ also has streams which collect the data into an internal buffer, allowing us to manipulate the data within the program.

Files

In C++, files are manipulated using the std::ifstream class for reading, std::ofstream class for writing, std::fstream class for reading and writing. For now, an ofstream is a special type of ostream, and thus is compatible with things that expect pointers or references to ostreams. A fstream is compatible with both ostream and istream, so we can use both << and >> on it.

String Streams

Sometimes we want to manipulate text in such a way that we can get the resulting string into a variable or extract the fields from a string variable. We can accomplish such behavior with the std::stringstream class.

If we want to use a string stream to “build up” a string into a variable, we can default construct one, and then use the << to add elements to the stream. Whenever we are done building the string up, we can use the .str() method to get a std::string with all the text we have inserted into the stream.

We cal also extract data from string stream with >>. As with other streams, the extraction could fail, so we must check for errors.

The two behaviors as not mutually exclusive-we can insert text into a string stream with << then extract it back out with >>.