Programming Language Concepts Using C and C++/Exception Handling in C++

From Wikibooks, open books for an open world
Jump to navigation Jump to search

Similar to Java, exceptions in C++ are most often—not always!—objects of a class type. That is, instead of returning a value of a certain type an exception object may be returned from the function. However, one can also throw an exception object of a primitive type. The following is an example to this unlikely case.

Example: Throwing an exception of a non-object type.

enum ERESULT { arg_neg = -1, arg_toobig = -2}; long fact(short n) { if (n < 0) throw arg_neg; if (n > MAX_ARG) throw arg_toobig; if (n == 0 || n == 1) return 1; else return (n * fact(n 1)); } // end of long fact(short)

Other peculiarities of exceptions in C++ are related to the way they are specified and handled. In addition to listing the exact list of exceptions thrown from a function by means of an exception specification, one can optionally remove the specification and get the liberty of throwing any exception.

Example: Exception specifications.

// f1 can throw exceptions of types E1 and E2 void f1(...) throw(E1, E2); // f2 does not throw any exceptions at all void f2(...) throw(); // f3 can throw exceptions of any type. This might be a good choice during the initial phases of a project. void f3(...);

If we explicitly list the exceptions thrown from a function and it turns out that an unexpected exception—that is, an exception that is not listed in the specification—is thrown and not handled in the function call chain, a call to unexpected(), defined in the C++ standard library, is made. In other words, detection of specification violations is carried out at run-time. If the control flow never reaches the point where the unexpected exception is thrown, program will run without a problem.

In line with its design philosophy C++ does not mandate that statements with a potential of throwing an exception be issued inside a try block. Similar to Java exceptions deriving from RuntimeException, C++ exceptions need not be guarded. In case we may be able to figure out that the exception never arises we can remove the try-catch keywords and get cleaner code.

Example: No mandatory try-catch blocks.

Rational divide_by_five(double x) { Rational rat1 = Rational(x); Rational nonzero_rat = Rational(5); Rational ret_rat = rat1.divide(nonzero_rat); return ret_rat; } // end of Rational divide_by_five(double)

Exception Class[edit]

Queue_Exceptions
1 #ifndef QUEUE_EXCEPTIONS_HXX
2 #define QUEUE_EXCEPTIONS_HXX
3 
4 #include <iostream>
5 using namespace std;
6 
7 namespace CSE224 {
8   namespace DS {
9     namespace Exceptions {

Note our exception class does not have any member fields. In other words, we have no means to identify details of the situation. All we know is we have a problem, nothing more! Although in our case we do not need any details about the nature of the problem, this is not always the case.

Take the factorial example for instance. We may want to pass the value of the argument that gave rise to the exceptional condition. This is equivalent to saying that we want to tell the difference between exception objects of the same class. As a matter of fact, we may formulate the problem as that of differentiating between objects of the same class, be that an exception object class or any other. We can do this simply by adding fields to the class definition.

Fact_Exceptions

class Negative_Arg { public: void error(void) { cerr << "Negative argument " << _arg_value << endl; } // end of void error(void) Negative_Arg(short arg) { _arg_value = arg; } private: short _arg_value; } // end of class Negative_Arg class TooBig_Arg { ... }

Factorial.cxx

... long fact(short n) throw(Negative_Arg, TooBig_Arg) { if (n < 0) throw Negative_Arg(n); if (n > MAX_ARG) throw TooBig_Arg(n); if (n == 0 || n == 1) return 1; else return(n * fact(n 1)); } // end of long fact(short) throw(Negative_Arg, TooBig_Arg) ...

10       class Queue_Empty {        
11       public:

Note the only function of our class has been declared to be static, which means we can invoke it without ever creating an instance of the class through the class scope operator. Similarly, one can define static data fields, which are shared by all instances of the class and there is no obligation to access these fields via objects of the class.

12         static void error(void) { cerr << "Queue Empty!!!" << endl; }
13       }; // end of class Queue_Empty
14     } // end of namespace Exceptions
15   } // end of namespace DS
16 } // end of namespace CSE224
17 
18 #endif

Module[edit]

Interface[edit]

Queue
 1 #ifndef QUEUE_HXX
 2 #define QUEUE_HXX
 3 
 4 #include <iostream>
 5 using namespace std;
 6 
 7 #include "ds/exceptions/Queue_Exceptions"
 8 using namespace CSE224::DS::Exceptions;
 9 
10 namespace CSE224 {
11   namespace DS {

What follows is a forward class declaration. Its purpose is similar to that of forward declaration ala C: we declare our intention of using a class named Queue_Node and defer its definition to some other place.

Note we cannot declare an object of this type. This is due to the fact that C++ does not let you declare variables to be of types whose definitions are not completed. Because compiler cannot figure out the amount of memory required for the object. However, we can declare variables to be pointers or references—read it as "constant pointers"—to such a class.

12     class Queue_Node;
13     class Queue {

In some cases, it is convenient to allow a certain function/class to access the non-public members of a class without allowing access to the other functions/classes in the program. The friend mechanism in C++ allows a class to grant functions/classes free access to its non-public members.

A friend declaration begins with the keyword friend. It may appear only within a class definition. In other words, it is the class that declares a function/class to be its friend, not the other way around. That is, you cannot simply declare a class as your friend and access its fields.

Since friends are not members of the class granting friendship, they are not affected by the public, protected, or private section in which they are declared within the class body. That is, friend declarations may appear anywhere in the class definition.

According to the following declaration, overloaded shift operator (<<) can freely access the internals of the Queue object, whose reference is passed as the second argument, as if they were public.

Had we chosen to make the shift operator into an instance function we would not have attained our goal. Take the following example:

cout << q1 << q2;

This statement will first print q1 and then q2 to the standard output file. We can reach the same effect by the following statements.

cout << q1; cout << q2;

As a matter of fact, this is what takes place behind the scene. We can see what’s happening by applying the following transformations.

cout << q1 << q2; cout.operator<<(q1).operator<<(q2); x.operator<<(q2);

The shift message is sent twice: once to the object named cout and once to the object returned by the first invocation of the appropriate function (x). This means we need to have a function signature where the value returned and the first argument are of the same type: ostream or ostream&. Knowing an instance function takes a pointer to an instance of the class being defined as its implicit first argument, we reach the conclusion that the shift operator cannot be an instance function of the Queue class. Way out of this is providing a friend declaration such as the following.

14       friend ostream& operator<<(ostream&, const Queue&);
15     public:
16       Queue(void) : _front(NULL), _rear(NULL), _size(0) { }
17       Queue(const Queue&);
18       ~Queue(void);
19       Queue& operator=(const Queue&);
20       bool operator==(const Queue&);

Note the types used in the exception specifications. The first function can abnormally return throwing a Queue_Empty object while the second one will return with a pointer to such an object. This should not come as a surprise. Unlike Java, which creates objects in the heap only, C++ lets you create your objects in all three regions—that is, the heap, the run-time stack, and the static data region. Since an exception object is basically a C++ object, you can create it in any data region you like.

Provided that you declare your exception handlers accordingly, there is not much of a difference between the following exception specifications.[1] Handler of the first one will expect an object, while the second one will expect a pointer that points to some area in the heap.[2]

21       double peek(void) throw(Queue_Empty);
22       double remove(void) throw(Queue_Empty*);
23       void insert(double);
24       bool empty(void);
25     private:
26       Queue_Node *_front, *_rear;
27       unsigned int _size;
28     }; // end of class Queue

Note all fields of the following class definition are private. There are no functions to manipulate the objects, either. So, it looks like we need some magic for creating and manipulating an object of the class. Answer lies in Queue_Node’s relation to Queue: Queue_Node is tightly coupled to Queue. A Queue_Node object can exist only within the context of a Queue object. This fact is reflected in the friend declaration. Thanks to this declaration, we can [indirectly] manipulate a Queue_Node object through operations on some Queue object.

30     class Queue_Node {
31       friend class Queue;

Next statement declares the shift operator to be a friend to the Queue_Node class. A similar declaration had been made in the Queue class, which means that one single function will have the privilege of peeking into the innards of two different classes.

32       friend ostream& operator<<(ostream&, const Queue&);
33     private:
34       double _item;
35         Queue_Node *_next;
36         Queue_Node(double val = 0) : _item(val), _next(NULL) { }
37     }; // end of class Queue_Node
38   } // end of namespace DS
39 } // end of namespace CSE224
40 
41 #endif

Implementation[edit]

Queue.cxx
 1 #include <iomanip>
 2 #include <iostream>
 3 using namespace std;
 4 
 5 #include "ds/Queue"
 6 #include "ds/exceptions/Queue_Exceptions"
 7 using namespace CSE224::DS::Exceptions;
 8 
 9 namespace CSE224 {
10   namespace DS {
11     Queue::
12     Queue(const Queue& rhs) : _front(NULL), _rear(NULL), _size(0) {
13       Queue_Node *ptr = rhs._front;
14       for(unsigned int i = 0; i < rhs._size; i++) { 
15         this->insert(ptr->_item);
16         ptr = ptr->_next;
17       } // end of for(unsigned int i = 0; i < rhs._size; i++)
18     } // end of copy constructor

Our destructor, implicitly invoked by the programmer (through delete in deallocating heap objects) or by the compiler-synthesized code (in the process of deallocating static and run-time stack objects), deletes all nodes in the queue and then proceeds with cleaning the room reserved for the fields. Had we forgotten to remove the items we would have ended up with the picture given below, which is actually the same picture we would have got without the destructor.

Creating garbage in the heap memory

Note the shaded region denotes the memory returned to the allocator by the delete operator itself, not the destructor.[3] All queue nodes reachable only through the fields in the shaded region have now become garbage. So, we must remove all queue items when the queue is deleted, which is what we do in the destructor body.

Note also we do not write the code within a try-catch block. Unlike Java, that’s OK with C++; you can choose to omit the try-catch block if you think they will never happen. In this case, the number of removals is guaranteed to be as many as the number of items in the queue and this cannot give rise to any exceptional condition.

 20     Queue::
 21     ~Queue(void) {
 22       unsigned int size = _size;
 23       for(unsigned int i = 0; i < size; i++) remove();
 24     } // end of destructor
 25 
 26     Queue& Queue::
 27     operator=(const Queue& rhs) {
 28       if (this == &rhs) return (*this);
 29 
 30       for(unsigned int i = _size; i > 0; i--) remove();
 31 
 32       Queue_Node *ptr = rhs._front;
 33       for(unsigned int i = 0; i < rhs._size; i++) { 
 34         this->insert(ptr->_item);
 35         ptr = ptr->_next;
 36       } // end of for(unsigned int i = 0; i < rhs._size; i++)
 37 
 38       if (rhs._size == 0) { 
 39         _front = _rear = NULL;
 40         _size = 0;
 41         return(*this);
 42       } // end of if(rhs._size == 0)
 43 
 44       return (*this);
 45     } // end of assignment operator
 46 
 47     bool Queue::
 48     operator==(const Queue& rhs) {
 49       if (_size != rhs._size) return false;
 50       if (_size == 0 || this == &rhs) return true;
 51 
 52       Queue_Node *ptr = _front;
 53       Queue_Node *ptr_rhs = rhs._front;
 54 
 55       for (unsigned int i = 0; i < _size; i++) {
 56         if (ptr->_item != ptr_rhs->_item) 
 57           return false;
 58         ptr = ptr->_next;
 59         ptr_rhs = ptr_rhs->_next;
 60       } // end of for(unsigned int i = 0; i < _size; i++)
 61 
 62       return true;
 63     } // end of equality-test operator
 64 
 65     double Queue::
 66     peek(void) throw(Queue_Empty) {
 67       if (empty()) throw Queue_Empty();
 68 
 69       return(_front->_item);
 70     } // end of double Queue::peek(void)
 71 
 72     double Queue::
 73     remove(void) throw(Queue_Empty*) {
 74       if (empty()) throw new Queue_Empty();
 75 
 76       double ret_val = _front->_item;
 77       Queue_Node *temp_node = _front;
 78 
 79       if (_front == _rear) _front = _rear = NULL;
 80         else _front = _front->_next;
 81 
 82       delete temp_node;
 83       _size--;
 84 
 85       return ret_val;
 86     } // end of double Queue::remove(void)
 87 
 88     void Queue::
 89     insert(double value) {
 90       Queue_Node *new_node = new Queue_Node(value);
 91 
 92       if (empty()) {
 93         _front = _rear = new_node;
 94         _size = 1;
 95         return;
 96       } // end of if (empty())
 97  
 98       _rear->_next = new_node;
 99       _rear = _rear->_next;
100       _size++;
101     } // end of void Queue::insert(double)
102 
103     bool Queue::
104     empty(void) { return (_size == 0); }

The following output operator definition makes use of both the Queue and the Queue_Node classes. It first prints the length of the queue by using a private field of the Queue class and then outputs the contents of the corresponding queue by traversing each and every node, which are of Queue_Node type. For this reason we had to make this function a friend to both classes.

106     ostream& operator<<(ostream& os, const Queue& rhs) {
107       os << "( " << rhs._size << " )";
108 
109       if (rhs._size == 0) {
110         os << endl;
111         return(os);
112       } // end of if (rhs._size == 0)
113 
114       os << "(front: ";
115       Queue_Node *iter = rhs._front;
116       while(iter != NULL) {
117         os << iter->_item << " ";
118         iter = iter->_next;
119       } // end of while(*iter != NULL)
120       os << " :rear )\n";
121 
122       return(os);
123     } // end of ostream& operator<<(ostream&, const Queue&)
124   } // end of namespace DS
125 } // end of namespace CSE224

Test Program[edit]

Queue_Test.cxx
 1 #include <fstream>
 2 #include <string>
 3 using namespace std;
 4 
 5 #include "ds/Queue"
 6 using namespace CSE224::DS;
 7 
 8 #include "ds/exceptions/Queue_Exceptions"
 9 using namespace CSE224::DS::Exceptions;
10 
11 int main(void) {
12   Queue q1;
13   string fname("Queue_Test.input");
14   ifstream infile(fname.c_str());
15 
16   if (!infile) {
17     cout << "Unable to open file: " << fname << endl;
18     return 1;
19   } // end of if(!infile)

Now that the argument to the handler (q) points to some heap memory, we must destroy the region as soon as we are done with handling the exception. That’s what we do with the delete operator inside the handler.

If we had preferred to pass an object instead of a pointer to an object, as we do in peek, there wouldn’t have been any need for such a clean-up activity; thanks to the code synthesized by the compiler, it would have been carried out automatically upon exit from the handler.

Observe we could have written the first statement of the handler as Queue_Empty::error(); This is OK because the sole function in our exception class is static, which means we can call it through the class name.

21   try { q1.remove(); } 
22     catch(Queue_Empty* q) { q->error(); delete q; }
23 
24   for (int i = 0; i < 10; i++) {
25     double val;
26     infile >> val;
27     q1.insert(val);
28   } // end of for(int i = 0; i < 10; i++)
29   infile.close();
30 
31   cout << q1;
32   Queue q2 = q1;
33 
34   cout << "Queue 1: " << q1;
35   cout << "Queue 2: " << q2;
36 
37   if (q1 == q2) cout << "OK" << endl; 
38     else cout << "Something wrong with equality testing!" << endl;
39 
40   q2.remove(); q2.remove();
41   cout << "Queue 2: " << q2;
42   if (q1 == q2) cout << "Something wrong with equality testing!" << endl;
43     else cout << "OK" << endl;
44 
45   return(0);
46 } // end of int main(void)

Input-Output in C++[edit]

Input-output facilities in C++, a component of the standard library, are provided by means of the iostream library, which is implemented as a class hierarchy that makes use of both multiple and virtual inheritance. This hierarchy includes classes dealing with input from and/or output to user's terminal, disk files, and memory buffers.

iostream library partial class hierarchy

The attributes of a particular stream type is somehow mangled in its name. For example, ifstream stands for a file stream that we us as a source of input. Similarly, ostringstream is an in-memory buffer—a string object—stream that is used as a sink of output.

The Base Stream Class: ios[edit]

Whatever the name of the class being used might be it eventually derives from ios, the base class of the iostream library. This class contains the functionality common to all streams, such as accessor-mutator functions for manipulating state and format. In the former group are included the following functions:

  • iostate rdstate() const: Returns the state of the current stream object, which can be any combination of the following: good, eof, fail, and bad.
  • void setstate(iostate new_state):In addition to the already set flags, sets the state of the stream to new_state. Note this function cannot be used to unset the flag values.
  • void clear(iostate new_state = ios::goodbit): Sets the state to the value passed in new_state.
  • int good(void): Returns true if the last operation on the stream was successful.
  • int eof(void): Returns true if the last operation on the stream found the end of file.
  • int fail(void): Returns true if the last operation on the stream was not successful and no data was lost due to the operation.
  • int bad(void): Returns true if the last operation on the stream was not successful and data was lost as a result of the operation.

For manipulating the format, we have

  • char fill(void) const: Returns the padding character currently in use. The default character is space.
  • char fill(char new_pad_char): Sets the padding character to new_pad_char and returns the previous value.
  • int precision(void) const: Returns the number of significant digits to be used for output of floating point numbers. The default value is 6.
  • int precision(int new_pre): Sets precision to new_pre and returns the previous value.
  • int width(void) const: Returns the output field width. Default value is 0, which means as many characters as necessary are used.
  • int width(int new_width): Sets width to new_width and returns the previous value.
  • fmtflags setf(fmtflags flag): Sets one of the flags, which are used to control the way output is produced. flag can have one of the following: (for base value used in output of integral values) ios::dec, ios::oct, ios::hex, (for displaying floating point values) ios::scientific, ios::fixed, (for justifying text) ios::left, ios::right, ios::internal, (for displaying extra information) ios::showbase, ios::showpoint, ios::showpos, ios::uppercase. As in the next four functions, this function returns the state that was in effect prior to the call.
  • fmtflags setf(fmtflags flag, fmtflags mask): Clears the combination of flags passed in mask and then sets the flags passed in flag.
  • fmtflags unsetf(fmtflags flag): Reverse of setf, this function makes sure the combination of flags passed in flag is not set.
  • fmtflags flags(void) const: Returns the current format state.
  • fmtflags flags(fmtflags new_flags): Sets the format state to new_flags.

Input Streams[edit]

On top of the functionality listed in the previous section, all input streams in C++ provide support for the following functions.

  • istream& operator>>(type data): Overloaded versions of the shift-in (or extraction) operator are used to read in values of various types and can further be overloaded by the programmer. It can be used in a cascaded manner and in case the input operation is unsuccessful it returns false, which means it can also be used in the context of a boolean expression.
  • int get(void): Returns the character under the read head and advances it by one.
  • int peek(void): Like the previous function, peek returns the character under the read head but doesn't move it. That is, peek does not alter the stream contents.
  • istream& get(char& c): Cascaded version of get(void), this function is equivalent to operator>>(char&). That is
in_str.get(c1).get(c2).get(c3);in_str >> c1 >> c2 >> c3;
  • istream& get(char* str, streamsize len, char delim = '\n'): Reads a null-terminated string into str. Length of this string depends on the second and third arguments, which hold the size of the buffer and the sentinel character, respectively. If len - 1 is scanned without reading the sentinel character, '\0' is appended to the buffer and returned in the first argument. If the sentinel character is reached before filling in the buffer, the read head is left on the sentinel character and all that has been read up to that point with the terminating '\0' is returned in the buffer.
  • istream& getline(ctype* str, streamsize len, char delim = '\n'): Similar to the previous function, getline is used to read a null-terminated string into its first argument. However, in case the sentinel character is reached before the buffer is filled, the sentinel character is not left in the stream but read and discarded. Note the type of the first argument is a pointer to one of char, unsigned char, or signed char.
  • istream& read(void* buf, streamsize len): Reads len bytes into buf, unless the input ends first. If input ends before len bytes are read this function sets the ios::fail flag and returns the incomplete result.
  • istream& putback(char c): Corresponding to ungetc(char) of C, this function attempts to back up one character and replace the character that has been backed up with c. Note this operation is guaranteed to work only once. Consecutive uses of it may or may not work.
  • istream& unget(void): Attempts to back up one character.
  • istream& ignore(streamsize len, char delim = traits::eof): This function reads and discards as many as len characters or all characters up to and including the delim.

Output Streams[edit]

Complementing the operations listed in the previous section are the operations applied on an output stream. Before we give a listing of these operations, we should mention one crucial point: in order for the output operations to take effect one of the following conditions must be met:

  1. An endl manipulator or '\n' is inserted into the stream.
  2. A flush manipulator is inserted into or a flush() message is sent to the stream.
  3. The buffer attached to the stream is full.
  4. An istream object tied to the stream performs an input operation. Tying two streams means their operations will be synchronized. A popular example is the cin-cout pair: before a message is sent to cin cout is flushed. That is,

cout << "Your name:"; cout << "Your name:"; cout.flush(); cout << "Your name" << flush; cin >> name; cin >> name; cin >> name;

  • ostream& operator<<(type data): Overloaded versions of the shift-out (or insertion) operator are used to write data of various types and can further be overloaded by the programmer. Like the extraction operator, it can be cascaded.
  • ostream& put(char c): Inserts c into the current stream.
  • ostream& write(string str, streamsize len): Inserts len characters of str into the current stream. Since a string object can be constructed from a [const] char*, the first argument can also be a C-style character string.

Before moving on to file-oriented streams, we should mention that functionalities of istream and ostream are combined in the iostream class, which derives from these two classes. That is, one can use the same stream for input and output at the same time.

File Input and Output[edit]

Using ifstream and ofstream one can read from and write to files. Since these classes inherit from the relevant stream classes—istream and ostream, respectively—their instances can receive the messages given in the previous sections. In addition to these one can also use the following list.

  • ifstream(const char* fn, int mde = ios::in, int prt = 644), ofstream(const char* fn, int mde = ios::out, int prt = 644): Connects the stream being constructed to the disk file named fn. The second and third arguments, which are optional, are used to specify the way the stream can be used. The third argument is specific to Unix-based operating systems and indicate the file protection bits. The second argument specifies how to open the disk file and can be a [reasonable] combination of the following:
    • ios::in: Opens the file for input and locates the read head at the beginning.
    • ios::out: Opens the file for output. While doing so, the file is truncated.
    • ios::app: Opens the file for output. File contents are not destroyed and each output operation inserts data to the end of the file.
    • ios::bin: Treats the file content as raw data. In environments where '\n' is mapped to a single byte this is not needed.
  • ifstream(void)& ofstream(void): Creates a stream object without connecting it to a disk file.
  • void open(const char* fn, int mde = def_mde, int prt = 644): Connects a previously constructed [disconnected] stream object to a disk file.
  • ios::pos_type tellg/tellp(void): Return the position of the file marker. The last letters, g for get and p for put, of these functions serve as a reminder of whether the file marker is a read head or a write head.
  • void seekg/seekp(pos_type n_p): These functions move the file marker—that is, the read or write head-to the absolute byte number specified by n_p. seekg—read it as "seek to a new location for the next get"—affects the read head while seekp—read it as "seek to a new location for the next put"—affects the write head.
  • void seekg/seekp(off_type offset, ios::seekdir dir): Move by as many as offset bytes relative to the location specified by dir, which can take one of the following values: [the beginning of the file] ios::beg, [the current file marker position] ios::cur, and [the end of the file] ios::end.

As a closing remark of this handout we should mention the possibility of simultaneously reading from and writing to the same file. In such a case, we can construct a fstream object and use it to achieve our goal.

Notes[edit]

  1. Actually, there is a difference. Be that a plain object or an exception object, an object created in the heap is managed by the programmer and must be freed by her
  2. As a matter of fact, you can pass a pointer to some area in other parts of the address space such as the static data or run-time stack regions. But then how are you going to decide whether to free the region or not? If it points to some place in the heap, it is the programmer’s responsibility and she must free the object; if the pointed object is not in the heap its lifetime will be managed by the compiler. We’d better be more deterministic and create all such objects in the heap or have the handler accept an extra argument. Or yet better, choose to pass objects, not pointers to object.
  3. Observe this is basically the same region that would have been returned with free in the case of a malloced heap object: the region pointed to by the pointer. This semblance leads us to an informal definition: delete operator is implicit invocation of the destructor plus free.