P.J. Plauger is senior editor of C/C++ Users Journal. He is Convener of the ISO C standards committee, WG14, and active on the C++ committee, WG21. His latest books are The Draft Standard C++ Library, and Programming on Purpose (three volumes), all published by Prentice-Hall. You can reach him at pjp@plauger.com.
Introduction
Encapsulating complex arithmetic is one of the earliest applications for C++. One of the first libraries supplied with a C++ translator supported iostream-style input and output, complex arithmetic, and little else. Since those early days, it has become traditional to provide at least minimal support for making complex look more like a built-in type in C++.Widespread current practice is to supply a header, such as complex.h, that defines at least a complex class based on type double. Addition, subtraction, and negation make easy inline functions. Multiplication and division are more involved, but still easy enough to supply. Other math functions are considerably harder. Support for them varies considerably.
Indeed, it is hard to find a better excuse for hiding implementation details of a class, or for overloading arithmetic operators, than providing complex arithmetic. A number of operators have well established meanings for complex values, dating back to the earliest days of Fortran. After integer and floating-point values, complex values probably have the richest culture in computer programming. Many scientists and engineers still bemoan the loss of complex data types in later languages such as Pascal and C. In a very real (and positive) sense, C++ allows C programs to look more like Fortran.
The header <complex> is rather more ambitious than the typical header <complex.h> of existing practice. It defines the template complex, which is accompanied by three specializations, one for each of the three floating-point types of Standard C: float, double, and long double. These specializations limit the permissible interconversions among the standard complex types, to discourage easy conversions that quietly lose precision.
The member objects are hidden inside each of these classes, as usual. In principle, the actual representation is equally hidden. But the functions that access stored values deliver only the Cartesian components of a complex value. Cartesian form represents a complex value as the pair (x, y) representing the sum x + i*y. Here, i is the notorious square root of -1, the unit vector along the imaginary axis in the complex plane. Thus, the real component is x and the imaginary component is y.
Another way to represent complex values is with polar components. Polar form represents a complex value as the pair (r, q) representing the product r*exp(i*q). Thus, the magnitude is r and the argument (or phase angle) is q. Put simply, Cartesian form is far more convenient for addition and subtraction, while polar form is sometimes more convenient for multiplication and division. On balance, however, Cartesian form is generally more convenient.
Beyond this brief introduction, I won't even try to explain all the mysteries of complex mathematics. Nor will I make a case for why you might want to use it. If you're familiar with complex math, you know it is often useful in its own right. It also models well many processes of interest to scientists and engineers, such as time-varying voltages and motion in two dimensions. But if you don't already know how you might use complex arithmetic, here is not the place for me to try to teach it.
Using <complex>
You include the header <complex> to make use of the template complex. (Listing 1 shows one way to write this header.) Most likely, you will use one of the three specializations complex<float>, complex<double>, or complex<long double>. Objects of any of these classes let you represent complex values to a given floating-point precision. Numerous template functions, usable with all three classes, perform many of the mathematical operations defined for complex values.The header defines the macro __STD_COMPLEX as a reassurance. Presumably, you can write code such as:
#if !defined(__STD_COMPLEX) #error WRONG COMPLEX LBBRARY #endifThe program should fail to translate if the implementation supplies a nonconforming version of the header <complex>. Unless you are very serious about writing highly portable code, you can probably omit such tests.You can construct an object of a floating-point class in several ways. Using scalar arguments, you can write:
complex<float> f0; // becomes (0.0F, 0.0F) complex<float> f1(3.0F); // becomes (3.0F, 0.0F) complex<float> f2(3.0, -2) // becomes (3.0F, -2.0F) complex<double> d0; // becomes (0.0, 0.0) complex<double> d1(3.0F); // becomes (3.0, 0.0) complex<double> d2(3.0, -2); // becomes (3.0, -2.0) complex<double> d3(f1); // becomes (3.0, 0.0) complex<long double> ld0; // becomes (0.0L, 0.0L) complex<long double> ld1(3.0F); // becomes (3.0L, 0.0L) complex<long double> ld2(3.0, -2); // becomes (3.0L, -2.0L)In other words, you can specify in a complex class constructor: no components, just the real component, or both real and imaginary components of a complex value. Any components you omit become zero.You can also construct a complex class object from a complex class object of any of the three precisions:
// becomes (3.0F, 0.0F) complex<float> f3(d1); // becomes (3.0F, -2.0F) complex<float> f4(ld2); // becomes (3.0, 0.0) complex<double> d3(f1); // becomes (3.0, -2.0) complex<double> d4(ld2); // becomes (3.0L, 0.0L) complex<long double> ld3(f1); // becomes (3.0L, -2.0L) complex<long double> ld4(d2);(I don't show the same-precision case, since that is the usual default copy constructor.)The various single-argument constructors also supply a number of useful implicit conversions, in a variety of contexts. Thus, for example, you can often write a double value (real component) or a complex<float> value where a complex<double> value is required.
Note, however, the use of the new keyword explicit in several of the constructors shown in Listing 1. This keyword explicitly prohibits the translator from using the constructor to make an implicit type conversion. Any constructors that quietly discard precision are so labeled. To convert to a complex class of lower precision, you have to write an explicit type cast. Naturally, such conversions can result in floating-point overflow, underflow, or loss of precision, in either or both components of the converted value. Use them cautiously.
All other functions are common to all three complex classes. For brevity in describing them, I introduce the following common notation:
I explain any other notation as needed.
- TC is the complex class, such as complex<float>.
- T is the corresponding floating-point type, such as float.
- x0 and x1 are objects of class TC.
- f is a value of type T.
The following arithmetic operators are overloaded for each of the complex classes. The operators have their usual arithmetic meaning, as extended into the complex plane:
x0 += x1 x0 + x1 x0 + f f + x0 x0 -= x1 x0 - x1 x0 - f f - x0 x0 *= x1 x0 * x1 x0 * f f* x0 x0 /= x1 x0 / x1 x0 / f f / x0 +x0 x0 == x1 x0 == f f == x0 -x0 x0 != x1 x0 != f f != x0A number of functions are also overloaded for each of the complex classes. One group returns a value of type T:
Note that a class TC also supplies member functions for accessing the stored components of the complex value. (Their presence has been on again, off again.) I prefer to use the above functions instead.
- abs(x0), returns the magnitude of x0 (the p polar component)
- arg(x0), returns the argument of x0 (the 0 polar component)
- imag(x0), returns the imaginary component of x0 (the y Cartesian component)
- norm(x0), returns the squared magnitude of x0 (x2 + y2)
- real(x0), returns the real component of x0 (the x Cartesian component)
Another group of functions returns a value of type TC:
Finally, you can insert and extract objects of class C. For example:
- conjg(x0), returns the conjugate of x0 (the value x-i*y) in Cartesian components)
- cos(x0), returns the cosine of x0
- cosh(x0), returns the hyperbolic cosine of x0
- exp(x0), returns the exponential of x0
- log(x0), returns the natural logarithm of x0
- polar(rho, theta), returns the complex value corresponding to the magnitude rho and argument theta, both of type F
- pow(x0, x1), returns x0 raised to the power x1
- pow(f, x1), returns f raised to the power x1
- pow(x0, f), returns x0 raised to the power f
- pow(x0, i), returns x0 raised to the power i, where i has type int
- sin(x0), returns the sine of x0
- sinh(x0), returns the hyperbolic sine of x0
- sqrt(x0), returns the square root of x0
cin >> x0extracts a pair of Cartesian component values from the standard input stream, constructs from them an object of class TC, and assigns it to x0. The components are enclosed in parentheses and separated by a comma, as in (2, -4.5).And last of all, you can insert an object of class TC into, say, the standard output stream, by writing:
cout << x0As you might expect, the components have a format acceptable to the extractor, above, such as (2, -4.5). Format flags and precision have their usual effect on each of the floating-point components. (See "Standard C: The Header <ios>,"CUJ, May 1994.) With a recent change, however, the field width applies to the entire complex number. The behavior is as if the value is first inserted into a string, then the string is inserted with the specified field width.
Loose Ends
The draft C++ Standard is still unclear on a few points. It does not say, for example, what other types you can instantiate template complex with (if any), nor what properties they must have. Nor does it give a hint about how to compute the complex hyperbolic cosine of such a type. Until such clarification is forthcoming, you should assume that the template merely provides a convenient naming convention for the three specializations.You will notice quite a few member template functions in Listing 1, as well. I don't know of any commercial C++ compilers that support this recent addition to the language. Naturally, a pragmatic implementation of the header <complex> today has to sidestep this problem. Next month, I'll discuss ways to implement this header and show some possible approaches.
This article is excerpted in part from P.J. Plauger, The Draft Standard C++ Library, (Englewood Cliffs, N.J.: Prentice-Hall, 1995).