9.15.5. Stopwatch Example

Simula 67, the first object-oriented programming language, was created to simplify computer simulations. The following example demonstrates how to simulate a simple device, a stopwatch, with C++ objects. Following the object-oriented approach, software developers model physical entities and intangible concepts by collecting their details in a class. They map the modeled entity's actions to the class's functions, and its data or information to its member variables. Sometimes it isn't clear whether developers should use a variable, a function, or both. Developers can write a correct program in many ways, so they choose the most straightforward way in cases like these.

A picture of an analog stopwatch with two buttons and two hands on the display.
A stopwatch modeling example. A simple device like a stopwatch has enough features to make it a good modeling example without becoming overwhelming. It has three features that interest us: (1) a start/stop button, (2) a reset button, and (3) a display.

In this example, we will model a stopwatch with two buttons and a display. The display, formed by the two hands and the numbers, displays the elapsed time between starting and stopping the watch. When pressed, the side button resets the display to 0. The top button performs two different operations depending on the watch's state: If stopped, the watch starts; if running, the watch stops. In general, the state of an object is the object's current condition or what the object is doing (e.g., stopped or running).

The Stopwatch Class

We begin by analyzing the problem. The stopwatch does something or performs some behavior when the user presses either of the buttons. Therefore, it's easy to model the two buttons with functions. In a graphical user interface (GUI) program, the display is always visible, but in a console-based program, we must add a function to display the elapsed time on demand. We implement the display with a variable and a function combination.

The design phase follows and builds on the analysis. The Stopwatch class currently specifies the three features we identified in the "real world." As we design the program, we discover that it needs two more member variables that analysis didn't identify and are not present in the UML diagram. It's common for a program to require features (variables and functions) beyond those in the "real world." During the design phase, we add these features, called implementation features. We add a variable to store when the Stopwatch object begins running. The time difference between when the watch stops and starts will give us the elapsed time. We also add a flag to represent the watch's state: running or stopped.

The first version of the Stopwatch UML class diagram:
Stopwatch
-----------------
-elapsed : double = 0
-----------------
+stime() : void
+reset() : void
+display() : void The second version Stopwatch UML class diagram:
Stopwatch
-----------------
-elapsed : double = 0
-stopped : bool = true
-timeb : start_time
-----------------
+stime() : void
+reset() : void
+display() : void
(a)(b)
The Stopwatch UML class diagram. The two UML class diagrams represent two phases of The Software Development Process: analysis and design. (See also Class Development). The stime function models the button at the top of the stopwatch: pressing it once starts the watch running; pressing it again stops it. The reset function models the reset button to the right of the start/stop button; pressing it resets the time to zero. The display function displays the elapsed time.
  1. A Stopwatch UML class diagram created during analysis. Each member in the class diagram corresponds to a feature identified in the problem domain, i.e., in a physical stopwatch.
  2. A modified Stopwatch UML class diagram updated during design. The stopped and start_time member variables don't appear in the original "real world" problem, but they are necessary for a software implementation.
When creating class diagrams, software developers attempt to make them independent of any programming language or operating system. Nevertheless, some data types (e.g., bool) are specific to a limited number of programming languages, and others (e.g., timeb) are specific to a limited number of operating systems. Programmers must translate these types to match the implementing language and operating environment.

State Machines

UML class diagrams represent the static structure of an object-oriented program. They specify the data that instances of the class are responsible for maintaining and the operations they can perform. The diagrams form the skeleton or armature of an object-oriented program - every part of the program attaches to and builds on the diagrammed classes. Some classes also exhibit dynamic behavior, meaning that what they do next depends on what they have done in the past. The UML represents dynamic behavior with statechart diagrams based on rigidly specified state machines.

State machines often recognize data patterns or respond to events such as button presses. How we describe some parts of a state machine depends on its purpose. A basic state machine consists of just a few simple parts, each a finite set of elements:

A state machine with two states: Stopped and Running. Two arrows, both labeled with 'stime,' connect the states. Two arrows, labeled 'reset' and 'display,' leave from and return to the 'Stopped' state.
The Stopwatch state machine. Two states fully characterize a Stopwatch object. The black dot with an arrow to the Stopped state designates Stopped as the initial or starting state. Two transitions leave and return to the Stopped state. These self-transitions respond to the corresponding button presses, complete the actions (i.e., call the named functions), and return to the Stopped state. A pair of transitions joins the Stopped and Running states. Pressing the stime button triggers each transition, causing the object to run its stime() function and toggle between states.

System Calls

When a program needs a service that doesn't have a language-specific operation, it must use a system call. Such is the case with the stopwatch program, which needs timing information. The program uses a system call that returns the amount of time since the epoch, the computer's beginning of time. On Unix, Linux, and macOS systems, the epoch is January 1, 1970; on Windows systems, it is January 1, 1980. In practice, the program defines a structure with two integer fields: time and millitm. The first field is the number of whole seconds since the epoch, and the second is the number of milliseconds. The program passes the structure by-pointer in the system call, and the operating system fills the structure.

  Structure System Call
Windows_timeb_ftime_s
Unix
Linux
macOS
timebftime
Getting the time since the epoch. The structures and the system calls needed to get the time since the epoch vary between operating systems, but the same #include directive incorporates them into a program: #include <sys/timeb.h>

Elaborated Source Code

The third phase of the software design process is implementation or programming. In a "real-world" situation, it's common for a customer to provide developers with a set of requirements they must adhere to. The absence of formal requirements or a physical watch to model allows the developers to adopt two arbitrary behaviors. First, it may be difficult to reset the time if a mechanical watch is running. So, in that case, we'll choose to do nothing. Second, a mechanical watch continuously displays the elapsed time. We could duplicate that behavior with a GUI program, but not one based on the console. So, we only show the time when the watch is stopped. If a problem specifies a particular behavior, we must follow the requirement; otherwise, we can choose how to implement an operation. If there is a customer, we should get approval (in writing) for our choices.

#include <iostream>
#include <sys/timeb.h>
using namespace std;


class Stopwatch
{
    private:												// (a)
        double      elapsed = 0;
        bool        stopped = true;
        _timeb      start_time;                    // Windows
        //timeb     start_time;                    // Linux / macOS

    public:
        //Stopwatch() : elapsed(0), stopped(true) {}							// (b)

        void reset()											// (c)
        {
            if (stopped)
                elapsed = 0;
        }

        void display()											// (d)
        {
            if (stopped)
                cout << "-------------" << endl << elapsed << endl << "-------------" << endl;
        }

        void stime();											// (e)

    private:
        double    floattime(_timeb t)             // Windows						// (f)
        //double  floattime(timeb t)              // Linux
        {
            return t.time + t.millitm / 1000.0;
        }
};
The Stopwatch class. When designing a class, choose attributes that differentiate objects. For example, height and weight are good attributes for a person class because they are well-known characteristics of people, and their values help to distinguish between them. Make other variables local to a function, but when, like start_time, two or more functions share data, make the variable a class member. Non-private attributes are rare. Most common desktop systems use the timeb structure, but its name varies.
  1. elapsed saves the total measured elapsed time; stopped saves the watch's current state; and start_time is the when the watch begins timing.
  2. An older way of initializing member variables - necessary before C++ allowed in-class initialization.
  3. The reset function models the reset button. It does nothing if the watch is running; otherwise, it resets the time to 0.
  4. display shows the time elapsed between starting and stopping the watch. The function does nothing if the watch is running.
  5. Models the start/stop button (see the following figure).
  6. floattime converts the seconds and milliseconds saved in a timeb structure to a single floating-point value, easing calculations.

 

void Stopwatch::stime()
{
    if (stopped)
    {
        _ftime_s(&start_time);          // Windows
        //ftime(&start_time);           // Linux
        stopped = false;
    }
    else
    {
        _timeb    end_time;              // Windows
        _ftime_s(&end_time);
        //timeb    end_time;             // Linux
        //ftime(&end_time);
        stopped = true;
        elapsed += floattime(end_time) - floattime(start_time);
    }
}
The Stopwatch stime function. The main stopwatch button starts the watch when it is stopped and stops it when it is running. The class models this behavior with the stime function. The first, or "true," if-statement branch handles the case when the watch is stopped, and the second, or "false," branch handles it when it is running. The _ftime_s (Windows) or ftime (Linux or macOS) function is a system call returning the time since the epoch. The difference between the two epoch intervals is the elapsed time.

start_time is a temporary value that doesn't characterize a Stopwatch object - it doesn't help describe an object or distinguish between two objects. However, it demonstrates how objects help limit variable scope. Each time the program calls stime, the function uses start_time, implying that the program must save it between function calls. The program can do this without objects by making the variable global or static. Making it global allows any function to modify it inappropriately; making it static prevents the program from simultaneously making multiple stopwatches.

 

int main()
{
    Stopwatch   sw;							// (a)
    char        op;

    do
    {
        cout << "S\tStart/Stop" << endl;				// (b)
        cout << "D\tDisplay" << endl;
        cout << "R\tReset" << endl;
        cout << "E\tExit" << endl;

        cin >> op;
        cin.ignore();

        switch (op)							// (c)
        {
            case 'S' :
            case 's' :
                sw.stime();
                break;

            case 'D' :
            case 'd' :
                sw.display();
                break;

            case 'R' :
            case 'r' :
                sw.reset();
                break;

            case 'E' :
            case 'e' :
                break;

            default:
                cerr << "Unrecognized operation" << endl;
                break;
        }
    } while (op != 'E' && op != 'e');					// (d)

    return 0;
}
main: The Stopwatch client. The client demonstrates how to instantiate and use a Stopwatch object.
  1. The program instantiates a Stopwatch object.
  2. The code prints a simple menu and reads the user's choice.
  3. The switch statement interprets the command the user selects.
  4. Loop until the user explicitly chooses to exit the program.

Downloadable Example Code

ViewDownloadComments
stopwatch.cpp stopwatch.cpp The Stopwatch program.