Programming Language Concepts Using C and C++/Exception Handling in C++
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#ifndef QUEUE_EXCEPTIONS_HXX
#define QUEUE_EXCEPTIONS_HXX
#include <iostream>
using namespace std;
namespace CSE224 {
namespace DS {
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.
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 { ... }
... 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) ...
class Queue_Empty {
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.
static void error(void) { cerr << "Queue Empty!!!" << endl; }
}; // end of class Queue_Empty
} // end of namespace Exceptions
} // end of namespace DS
} // end of namespace CSE224
#endif
Module
editInterface
edit#ifndef QUEUE_HXX
#define QUEUE_HXX
#include <iostream>
using namespace std;
#include "ds/exceptions/Queue_Exceptions"
using namespace CSE224::DS::Exceptions;
namespace CSE224 {
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.
class Queue_Node;
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.
friend ostream& operator<<(ostream&, const Queue&);
public:
Queue(void) : _front(NULL), _rear(NULL), _size(0) { }
Queue(const Queue&);
~Queue(void);
Queue& operator=(const Queue&);
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]
double peek(void) throw(Queue_Empty);
double remove(void) throw(Queue_Empty*);
void insert(double);
bool empty(void);
private:
Queue_Node *_front, *_rear;
unsigned int _size;
}; // 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.
class Queue_Node {
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.
friend ostream& operator<<(ostream&, const Queue&);
private:
double _item;
Queue_Node *_next;
Queue_Node(double val = 0) : _item(val), _next(NULL) { }
}; // end of class Queue_Node
} // end of namespace DS
} // end of namespace CSE224
#endif
Implementation
edit#include <iomanip>
#include <iostream>
using namespace std;
#include "ds/Queue"
#include "ds/exceptions/Queue_Exceptions"
using namespace CSE224::DS::Exceptions;
namespace CSE224 {
namespace DS {
Queue::
Queue(const Queue& rhs) : _front(NULL), _rear(NULL), _size(0) {
Queue_Node *ptr = rhs._front;
for(unsigned int i = 0; i < rhs._size; i++) {
this->insert(ptr->_item);
ptr = ptr->_next;
} // end of for(unsigned int i = 0; i < rhs._size; i++)
} // 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.
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.
Queue::
~Queue(void) {
unsigned int size = _size;
for(unsigned int i = 0; i < size; i++) remove();
} // end of destructor
Queue& Queue::
operator=(const Queue& rhs) {
if (this == &rhs) return (*this);
for(unsigned int i = _size; i > 0; i--) remove();
Queue_Node *ptr = rhs._front;
for(unsigned int i = 0; i < rhs._size; i++) {
this->insert(ptr->_item);
ptr = ptr->_next;
} // end of for(unsigned int i = 0; i < rhs._size; i++)
if (rhs._size == 0) {
_front = _rear = NULL;
_size = 0;
return(*this);
} // end of if(rhs._size == 0)
return (*this);
} // end of assignment operator
bool Queue::
operator==(const Queue& rhs) {
if (_size != rhs._size) return false;
if (_size == 0 || this == &rhs) return true;
Queue_Node *ptr = _front;
Queue_Node *ptr_rhs = rhs._front;
for (unsigned int i = 0; i < _size; i++) {
if (ptr->_item != ptr_rhs->_item)
return false;
ptr = ptr->_next;
ptr_rhs = ptr_rhs->_next;
} // end of for(unsigned int i = 0; i < _size; i++)
return true;
} // end of equality-test operator
double Queue::
peek(void) throw(Queue_Empty) {
if (empty()) throw Queue_Empty();
return(_front->_item);
} // end of double Queue::peek(void)
double Queue::
remove(void) throw(Queue_Empty*) {
if (empty()) throw new Queue_Empty();
double ret_val = _front->_item;
Queue_Node *temp_node = _front;
if (_front == _rear) _front = _rear = NULL;
else _front = _front->_next;
delete temp_node;
_size--;
return ret_val;
} // end of double Queue::remove(void)
void Queue::
insert(double value) {
Queue_Node *new_node = new Queue_Node(value);
if (empty()) {
_front = _rear = new_node;
_size = 1;
return;
} // end of if (empty())
_rear->_next = new_node;
_rear = _rear->_next;
_size++;
} // end of void Queue::insert(double)
bool Queue::
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.
ostream& operator<<(ostream& os, const Queue& rhs) {
os << "( " << rhs._size << " )";
if (rhs._size == 0) {
os << endl;
return(os);
} // end of if (rhs._size == 0)
os << "(front: ";
Queue_Node *iter = rhs._front;
while(iter != NULL) {
os << iter->_item << " ";
iter = iter->_next;
} // end of while(*iter != NULL)
os << " :rear )\n";
return(os);
} // end of ostream& operator<<(ostream&, const Queue&)
} // end of namespace DS
} // end of namespace CSE224
Test Program
edit#include <fstream>
#include <string>
using namespace std;
#include "ds/Queue"
using namespace CSE224::DS;
#include "ds/exceptions/Queue_Exceptions"
using namespace CSE224::DS::Exceptions;
int main(void) {
Queue q1;
string fname("Queue_Test.input");
ifstream infile(fname.c_str());
if (!infile) {
cout << "Unable to open file: " << fname << endl;
return 1;
} // 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.
try { q1.remove(); }
catch(Queue_Empty* q) { q->error(); delete q; }
for (int i = 0; i < 10; i++) {
double val;
infile >> val;
q1.insert(val);
} // end of for(int i = 0; i < 10; i++)
infile.close();
cout << q1;
Queue q2 = q1;
cout << "Queue 1: " << q1;
cout << "Queue 2: " << q2;
if (q1 == q2) cout << "OK" << endl;
else cout << "Something wrong with equality testing!" << endl;
q2.remove(); q2.remove();
cout << "Queue 2: " << q2;
if (q1 == q2) cout << "Something wrong with equality testing!" << endl;
else cout << "OK" << endl;
return(0);
} // end of int main(void)
Input-Output in C++
editInput-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.
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
, andbad
.void setstate(iostate new_state)
:In addition to the already set flags, sets the state of the stream tonew_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 innew_state
.int good(void)
: Returnstrue
if the last operation on the stream was successful.int eof(void)
: Returnstrue
if the last operation on the stream found the end of file.int fail(void)
: Returnstrue
if the last operation on the stream was not successful and no data was lost due to the operation.int bad(void)
: Returnstrue
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 tonew_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 tonew_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 tonew_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 inflag
.fmtflags unsetf(fmtflags flag)
: Reverse ofsetf
, this function makes sure the combination of flags passed inflag
is not set.fmtflags flags(void) const
: Returns the current format state.fmtflags flags(fmtflags new_flags)
: Sets the format state tonew_flags
.
Input Streams
editOn 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 returnsfalse
, 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 ofget(void)
, this function is equivalent tooperator>>(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 anull
-terminated string intostr
. Length of this string depends on the second and third arguments, which hold the size of the buffer and the sentinel character, respectively. Iflen - 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 anull
-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 ofchar
,unsigned char
, orsigned char
.istream& read(void* buf, streamsize len)
: Readslen
bytes intobuf
, unless the input ends first. If input ends beforelen
bytes are read this function sets theios::fail
flag and returns the incomplete result.istream& putback(char c)
: Corresponding toungetc(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 aslen
characters or all characters up to and including thedelim
.
Output Streams
editComplementing 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:
- An
endl
manipulator or'\n'
is inserted into the stream. - A
flush
manipulator is inserted into or aflush()
message is sent to the stream. - The buffer attached to the stream is full.
- An
istream
object tied to the stream performs an input operation. Tying two streams means their operations will be synchronized. A popular example is thecin
-cout
pair: before a message is sent tocin
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)
: Insertsc
into the current stream.ostream& write(string str, streamsize len)
: Insertslen
characters ofstr
into the current stream. Since astring
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
editUsing 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 namedfn
. 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 andp
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 byn_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 asoffset
bytes relative to the location specified bydir
, 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- ↑ 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
- ↑ 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.
- ↑ Observe this is basically the same region that would have been returned with
free
in the case of amalloc
ed 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 plusfree
.