8.6. Bulletproof Code: Strings → Numbers

Time: 00:05:48 | Download: Large, LargeCC, Small | Streaming, Streaming (CC) | Slides (PDF)
Review

By this point in our study of programming generally and C++ specifically, we know that programs often require the user to input data. Furthermore, we know that there are many kinds of data and that entering the wrong kind can lead to strange results or program failure (infinite loops, crashes, etc.). How a program responds when the end-user enters the wrong kind of data depends on what kind of data the program expects versus what kind of data the user entered, the programming language, and the host system (hardware and operating system). The following program illustrates some of these variables.

#include <iostream>
using namespace std;

int main()
{
	int	input;
	cout << "Please enter an integer: ";
	cin >> input;

	cout << input;

	return 0;
}
D:\>bulletproof
Please enter an integer: hello
4194048
D:\>
D:\>bulletproof
Please enter an integer: hello
2096640
D:\>
(a)(b)
A data input error. The ANSI standard requires that correct programs must always behave in the same, consistent way, but the standard does not specify the behavior of incorrect programs. Incorrect programs may crash or produce incorrect or inconsistent results.
  1. A simple test program named bulletproof.cpp that expects the user to enter an integer.
  2. Each time the program runs, a string is entered rather than an integer. Although we enter the same string during each run, the program prints a different value: the address of the string in memory.

Bulletproof Code

Bulletproof code is code that catches or detects input errors, reports the error to the user, and "gracefully" deals with the error. Gracefully dealing with an error typically means that after detecting the error and printing a diagnostic (i.e., an error message), the program either terminates or allows the user to re-enter the data. Detecting the error is usually the hardest part of this process, so we approach it by breaking it into two smaller problems.

First, entering the wrong kind of data can cause a program to fail. We need a way to input the data that is guaranteed not to crash the program. A program can represent all data as a string (either a C-string or an instance of the string class). We use strings as our "universal" data type and initially read the data as a string. Once the data is validated, there are various ways that the data can be re-read from the string. We'll explore some of these techniques in the Streams chapter, but for simplicity, we'll restrict this discussion to just numbers.

Once the data is in the program as a string, we need to verify that each character in the string represents the kind of data that the program needs. For example, the string "123" looks like an integer, but the string "hello" does not. We need some way of formalizing and programming the concept of "looks like." Regular expressions (RE) are a compact and efficient way of verifying that strings match a given pattern. RE can differentiate between a string of digit characters and non-digit characters or a string with one non-digit character amongst digits. RE can detect and validate that data are in the correct format. For example, imagine a program prompts a user to enter a date. An RE can distinguish between Jan 1, 2025 and 1/1/2025, and branch to the correct code to process the input. Unfortunately, REs are a bit advanced for us right now and beyond the scope of this course, but you will learn about them later. Although not as compact or powerful as an RE, we can use loops and the cctype library to detect non-numeric input.

string input;
getline(cin, input);

for (size_t i = 0; i < input.length(); i++)
    if (! isdigit(input[i]))
    {
        cerr << "Invalid integer: " << input << endl;
        exit(1);
    }
char input[100];
cin.getline(input, 100);

for (size_t i = 0; i < strlen(input); i++)
    if (! isdigit(input[i]))
    {
        cerr << "Invalid integer: " << input << endl;
        exit(1);
    }
(a)(b)
Validating integer input. isdigit returns true whenever its argument is a decimal digit (i.e., '0' to '9'). The program prints an error message and terminates if any character in the string is not a digit (notice the !). We ignore the possible presence of a '-' indicating a negative number for simplicity.
  1. string class version
  2. C-string version

If the string only contains digits, we can safely convert it to an integer. Validating a correct floating-point number (either a float or a double) is more difficult because it may have a decimal point or an exponent. Nevertheless, converting a string to a floating-point number is almost as easy as converting a string to an integer.

Converting Strings To Numbers

We can use the casting operators to convert from one numeric data type to another. But casting works only when the source and destination data types are "close" (e.g., an integer and a double are "close" in the sense that they are both numbers). But strings (either C-strings or string objects) are too different - not "close" enough - to cast between them. Nevertheless, we know from the solutions developed for the palindrome-number problem (cpalnumber.cpp and palnumber.cpp) that is is possible to convert an integer into a string. Converting a correctly formed string into a number is equally easy.

string ClassC-String
#include <iostream>
#include <string>
#include <cctype>
using namespace std;

int main()
{
    string    input;
    cout << "Please enter an integer: ";
    getline(cin, input);

    for (size_t i = 0; i < input.length(); i++)
        if (!  isdigit(input[i]))
        {
            cerr << "Invalid integer: " << input << endl;
            exit(1);
        }

    cout << stoi(input) << endl;

    return 0;
}
#include <iostream>
#include <cstring>
#include <cctype>
using namespace std;

int main()
{
    char    input[100];
    cout << "Please enter an integer: ";
    cin.getline(input, 100);

    for (size_t i = 0; i < strlen(input); i++)
        if (!  isdigit(input[i]))
        {
            cerr << "Invalid integer: " << input << endl;
            exit(1);
        }

    cout << atoi(input) << endl;

    return 0;
}
(a)(b)
D:\>bulletproof
Please enter an integer: 123
123
D:\>bulletproof
Please enter an integer: hello
Invalid integer: hello
(c)(d)
Bulletproof input: Converting a string to an int. Bulletproof code reads user input as a string. The program tests each character in the string with the isdigit function. The program prints an error message and terminates if a character is not in the range ['0' - '9']. The program converts the string to an integer if all the characters are digits.
  1. Code specific to the string class is highlighted in yellow
  2. Code specific to C-strings is highlighted in yellow
  3. Output from both versions when the user enters a valid integer
  4. Output from both versions when the program cannot convert the input to an integer

Conversion Functions And Documentation

The documentation for the string class conversion functions show function prototypes with multiple parameters. However, only the first parameter lacks a default value, so the functions work as illustrated here. The impact of the default arguments is easier to see in the following two figures.

string to int int stoi(const string& s, size_t* index = nullptr, int base = 10);
string to double double stod(const string& s, size_t* index = nullptr);
String conversion functions. C++ documentation typically provides information about how to use a function in the form of a function prototype.
string to intstring to double
#include <iostream>
#include <string>
using namespace std;

int main()
{
    string	s = "1234Hello World";
    size_t	index = 0;

    cout << stoi(s) << endl;		// a
    cout << stoi(s, nullptr) << endl;	// b
    cout << stoi(s, &index) << endl;	// c
    cout << index << endl;		// d

    return 0;
}
#include <iostream>
#include <string>
using namespace std;

int main()
{
    string	s = "3.14Hello World";
    size_t	index = 0;

    cout << stod(s) << endl;		// a
    cout << stod(s, nullptr) << endl;	// b
    cout << stod(s, &index) << endl;	// c
    cout << index << endl;		// d

    return 0;
}
OutputOutput
1235
1235
1235
4
3.14
3.14
3.14
4
Advanced string conversion function examples.
  1. Simple call; defaults used for the second and third arguments.
  2. Emphasizes, for illustration, that the second argument is a pointer. It's more common for programmers to accept the default argument than to explicitly pass nullptr.
  3. Passes a pointer to index. The function operates on the string from left to right, saving the position in the string of the first character it could not convert to a number in index.
  4. The digit "1" is at index location 0, so "H" is at index location 4 in the string, which corresponds to the output.

For the C-string conversion functions that take multiple arguments, notice that the second argument is a character pointer - not an integer pointer as in the string class functions. The character and integer pointers serve the same purpose: notifying the program where the string-to-number conversion ended in the string. Whereas the string class functions provide an index into the string, the C-string functions return a sub-string (a character pointer to the beginning of the unconverted part of the original string). C++ inherits the the C-string conversion functions from the C Programming Language, which does not support default arguments. So, programmers must provide arguments for all the conversion functions' parameters. However, the following figures demonstrate that nullptr is a valid second argument for the multi-argument functions.

C-string to int &
long int
int atoi(const char* s);
long strtol(const char* s, char** end, int base);
C-string to double double atof(const char* s);
double strtod(const char* s, char** end);
C-string conversion functions. Example prototypes for the C/C++ string-to-numbers conversion functions.
int / longdouble
#include <iostream>
#include <cstring>
using namespace std;

int main()
{
    char*	input = "1234Hello World";

    cout << atoi(input) << endl;		// a

    char* end;
    cout << strtol(input, nullptr, 10) << endl;	// b
    cout << strtol(input, &end, 10) << endl;	// c
    cout << end << endl;			// d

    return 0;
}
#include <iostream>
#include <cstring>
using namespace std;

int main()
{
    char*	input = "3.14Hello World";

    cout << atof(input) << endl;		// a

    char* end;
    cout << strtod(input, nullptr) << endl;	// b
    cout << strtod(input, &end) << endl;	// c
    cout << end << endl;			// d

    return 0;
}
OutputOutput
1234
1234
1234
Hello World
3.14
3.14
3.14
Hello World
C-string conversion functions examples.
  1. Simple call.
  2. The function requires three parameters; if you wish to "ignore" the second parameter, you must explicitly pass a null pointer as the parameter.
  3. Passes a C-string as the second parameter; the function operates on the C-string left to right; end points to the first character in the string that the function could not convert to a number.
  4. After the function call, end points to the C-string "Hello World"