Conversion Functions
The standard library has several functions, in the <string> header, that will convert a C++ string to a number (much like the C++11 to_string() function will convert a number to a string.)
- the stoi(string) function returns an int
- the stod(string) function returns a double
- There are also stof(), stol(), stoul() and stold() for the types float, long, unsigned long and long double respectively.
These functions don't appear in C++ 98. If your employer is using an older version of C++, and doesn't want to change to a later version, for a wide (and reasonable) variety of reasons. What can you do? Implement them yourself, of course.
This is not a made-up scenario; we are learning about the latest version of C++ here in CS 150, but in the real world, you'll need to be prepared for older versions.
Using the Conversion Functions
Your goal is to be able to compile exactly the same file in C++98, C++11, C++14 and C++17, and to get the same results each time. Click the "running-man" icon on the right to find our test program in Replit. Click the Fork Repl button so you have your own copy.
Switch to Shell window on the right and type make 17. You'll see that the program compiles and runs (although it terminates with an error when trying to convert "UB-40".)
Now, type make 98 in the Shell. This will compile your program with C++98.
OOPS! That doesn't look encouraging! What's wrong???
C++98 does not have those functions, so you get an undeclared identifier. You can fix that by implementing these two functions yourself, using the string stream classes.
Stub the Replacements
Place these definitions for the functions right above main.
int stoi(const string& str) { return 0; }
double stod(const string& str) { return 0.0; }
Now, type make 98 once again. You'll see that the code compiles and "runs" (although your stubs don't produce the correct value, of course).
What happens when you upgrade to Visual Studio 19? Will the code still compile? Nope! Your version of stoi() conflicts with the one already defined inside the new C++ standard library; we get a clash of symbols.
At link time, there can be only one copy of the stoi() function in the executable; if the library already has one, your program won't link. This is called the ODR or One Definition Rule.
What you would like to say is: "if I'm using C++11 or later use the library version, and, if I'm using an older version of C++, then use the version which I've written". You can do that with conditional compilation.
Using #define
One capability of the preprocessor is to substitute one portion of text for another, before your code is compiled. You do this with the preprocessor directive #define. Here's an example:
#define PI 3.14159265358979323846
This is called a #defined constant. Note that you do not use an equals sign when creating these constants. By convention, constants and functions (called MACROs) created by the preprocessor are named using all-caps.
With PI previously defined, if you write this fragment:
cout << PI << endl;
The preprocessor replaces the symbol PI with its predefined value before sending it to the compiler, which only sees this:
cout << 3.14159265358979323846 << endl;
Of course, in this case, a better solution is to just used const, which wasn't available in C. In C++, we discourage the use excessive use of the preprocessor
because it can lead to unreadable, hard-to-maintain code, as well as hiding bugs beneath several layers of obfuscation. Of course, some of you may revel in that, so you'll want to look at the winners of the annual Obfuscated C Code Contest.The one place where we still use #define in C++ is for the use of source control using conditional compilation. We'll look at that next.
Conditional Compilation
For source code control, you can #define a symbol and not give it a value. The preprocessor supports conditional compilation, using preprocessor directives to conditionally include a section of code based on which #defined symbols it has "seen":
#if defined(A)
cout << "A is defined." << endl;
#elif defined(B)
cout << "B is defined." << endl;
#else
cout << "Neither A or B is defined." << endl;
#endif
This code is processed before the rest of the code is sent to the compiler; these conditions can only refer to #defined constants, integer values, and arithmetic and logical expressions using those values.
Here we've used the predefined preprocessor function defined() to check if a constant has been previously been #defined. You can also use these "shorthand" expressions.
- #ifdef is short for if defined; #ifdef symbol is the same as #if defined(symbol).
- #ifndef is short for if not defined, the opposite of #ifdef.
Conditional compilation determines whether pieces of code are sent to the compiler. When the preprocessor encounters this, whichever conditional expression is true will have its corresponding code block included in the final program. If your code previously encountered #define A, then this entire portion of code will go to the compiler as:
cout << "A is defined." << endl;
The rest of the code will simply be discarded.
Predefined Symbols
There are several predefined symbols which your toolchain supplies, and which you can use in conditional compilation, like this example from StackOverflow, which tests for compiling on different platforms:
#ifdef _WIN32
//define something for Windows (32-bit)
#elif __APPLE__
// define something for OSX
#elif __linux
// linux
#endif
These predefined symbols include those that are standard on every version of C++ and those that are common to GCC on every platform. There are also platform-specific symbols for other toolchains (such as the operating system). You can get a list of those by running cpp -dM from the shell.
For this problem, we care about is a particular version of C++. In the list of predefined standard constants, you'll see that __cplusplus (double leading underscores) contains version numbers for each release of C++. You can use that to bracket your own versions of the stoi() and stod() functions.
Go back to your test program and use this facility to define the functions only if the symbol __cplusplus is <= 199711L. Now you can compile and run with C++98 and with C++11/14/17/20 using the same source.
To implement the functions, just use code like this:
function stoi <- input str -> output int set result to 0 construct an input string stream using str read from str into result return result
The stod() function will be identical, except result will be double instead of int.
Comparing the Results
Now, go ahead and you run the test program. Use make 17 to compile and run under C++ 17, and make 98 to compile and run under C++ 98. Your code should compile under both platforms. When you run it, however, it doesn't produce exactly the same output as it does under C++17, which uses the stoi() from the standard library.
Look at the lines hightlighted in yellow, where we pass stod() or stoi() invalid input. C++17 and C++98 produce the same output for the first four inputs, but the last one fails entirely. Neither the library nor your version fails on stoi("3.14159"). Both convert what they can (the 3) and leaves the rest. But, the library version crashes with stoi("UB-40"); there is no possible conversion.
So, that means the version we wrote is better, right?
After all, who wants a function that crashes?
Well, not so fast. The question is, what should stod() and stoi() do with invalid input? In the next lesson, we'll use these techniques to look at more error handling.