2.4 Modular Programming
Over the years, the emphasis in the design of programs has shifted from the design of procedures and toward the organization of data. Among other things, this reflects an increase in program size. A set of related procedures with the data they manipulate is often called a module. The programming paradigm becomes this:
Decide which modules you want; partition the program so that data is hidden within modules.
This paradigm is also known as the data-hiding principle. Where there is no grouping of procedures with related data, the procedural programming style suffices. Also, the techniques for designing "good procedures" are now applied for each procedure in a module. The most common example of a module is the definition of a stack. These are the main problems that have to be solved:
Provide a user interface for the stack (for example, functions push() and pop()).
Ensure that the representation of the stack (such as an array of elements) can be accessed only through this user interface.
Ensure that the stack is initialized before its first use.
C++ provides a mechanism for grouping related data, functions, and so on, into separate namespaces. For example, the user interface of a Stack module could be declared and used like this:
namespace Stack{ // interface void push(char); char pop(); } void f() { Stack::push('c'); if (Stack::pop() != 'c') error("impossible"); }
The Stack:: qualification indicates that the push() and pop() are those from the Stack namespace. Other uses of those names will not interfere or cause confusion.
The definition of the Stack could be provided in a separately compiled part of the program:
namespace Stack{ // implementation const int max_size = 200; char v[max_size]; int top = 0; void push(char c) { /* check for overflow and push c */ } char pop() { /* check for underflow and pop */ } }
The key point about this Stack module is that the user code is insulated from the data representation of Stack by the code implementing Stack::push() and Stack::pop(). The user doesn't need to know that the Stack is implemented using an array, and the implementation can be changed without affecting user code. The /* starts a comment that extends to the following */.
Because data is only one of the things one might want to "hide," the notion of data hiding is trivially extended to the notion of information hiding; that is, the names of functions, types, and so on, can also be made local to a module. Consequently, C++ allows any declaration to be placed in a namespace.
This Stack module is one way of representing a stack. The following sections use a variety of stacks to illustrate different programming styles.
2.4.1 Separate Compilation
C++ supports C's notion of separate compilation. This can be used to organize a program into a set of semi-independent fragments.
Typically, we place the declarations that specify the interface to a module in a file with a name indicating its intended use. Thus,
namespace Stack{ // interface void push(char); char pop(); }
would be placed in a file stack.h, and users will include that file, called a header file, like this:
#include "stack.h" // get the interface void f() { Stack::push('c'); if (Stack::pop() != 'c') error("impossible"); }
To help the compiler ensure consistency, the file providing the implementation of the Stack module will also include the interface:
#include "stack.h" // get the interface namespace Stack{ // representation const int max_size = 200; char v[max_size]; int top = 0; } void Stack::push(char c) { /* check for overflow and push c */ } char Stack::pop() { /* check for underflow and pop */ }
The user code goes in a third file, say user.c. The code in user.c and stack.c shares the stack interface information presented in stack.h, but the two files are otherwise independent and can be separately compiled. Graphically, the program fragments can be represented like this:
Separate compilation is an issue in all real programs. It is not simply a concern in programs that present facilities, such as a Stack, as modules. Strictly speaking, using separate compilation isn't a language issue; it is an issue of how best to take advantage of a particular language implementation. However, it is of great practical importance. The best approach is to maximize modularity, represent that modularity logically through language features, and then exploit the modularity physically through files for effective separate compilation.
2.4.2 Exception Handling
When a program is designed as a set of modules, error handling must be considered in light of these modules. Which module is responsible for handling what errors? Often, the module that detects an error doesn't know what action to take. The recovery action depends on the module that invoked the operation rather than on the module that found the error while trying to perform the operation. As programs grow, and especially when libraries are used extensively, standards for handling errors (or, more generally, "exceptional circumstances") become important.
Consider again the Stack example. What ought to be done when we try to push() one too many characters? The writer of the Stack module doesn't know what the user would like to be done in this case, and the user cannot consistently detect the problem (if the user could, the overflow wouldn't happen in the first place). The solution is for the Stack implementer to detect the overflow and then tell the (unknown) user. The user can then take appropriate action. For example:
namespace Stack{ // interface void push(char); char pop(); class Overflow { }; // type representing overflow exceptions }
When detecting an overflow, Stack::push() can invoke the exception-handling code; that is, "throw an Overflow exception:"
void Stack::push(char c) { if (top == max_size) throw Overflow(); // push c }
The throw transfers control to a handler for exceptions of type Stack::Overflow in some function that directly or indirectly called Stack::push(). To do that, the implementation will unwind the function call stack as needed to get back to the context of that caller. Thus, the throw acts as a multilevel return. For example:
void f() { // ... try { // exceptions here are handled by the handler defined below while (true) Stack::push('c'); } catch (Stack::Overflow) { // oops: stack overflow; take appropriate action } // ... }
The while loop will try to loop forever. Therefore, the catch-clause providing a handler for Stack::Overflow will be entered after some call of Stack::push() causes a throw.
Use of the exception-handling mechanisms can make error handling more regular and readable.