While complex numbers are simple in theory, it should be no surprise that in practice, theyre well, complex.
Scientific computing has a long history, and, like most computer languages, C++ includes features to support the special needs of scientists. One way in which C++ differs from such languages as Fortran, however, is that much of this support is in the library rather than in the core language. One of the basic guiding principles of C++ (it inherited this principle from C) was that the language should focus on general abstraction mechanisms: if you can think of a facility that ought to be supported, then its likely that other people will need two or three other similar facilities that you havent thought of. Rather than building just one of them into the language, its better to build in general mechanisms so that all of them, and more, can be expressed as libraries.
One illustration of this general dictum is C++s support for complex numbers. Mathematically, the easiest way to think about complex numbers is that a complex number has a real part and an imaginary part: z = x + i y, where x and y are ordinary real numbers and where i2 = -1. All of the ordinary rules of arithmetic apply. For example:
z1 z2 = (x1 + i y1) (x2 + i y2) = (x1 x2 - y1 y2) + i(x1 y2 + y1 x2)
We can of course think of the ordered pair (x, y) as a point on the plane, and, again of course, we can choose to represent that point in polar rather than rectangular coordinates: instead of the ordered pair (x, y), we can use the ordered pair (r, q). One of the fundamental equations of complex arithmetic is the relation between those two representations:
z = reiq = r cos q + i r sin q
Complex numbers are ubiquitous: they appear in the theory of equations (an nth order polynomial always has n complex roots), in the analysis of special functions, in quantum mechanics (the wave amplitude in the Schrödinger equation is complex), in electrical engineering, and in digital signal processing. Practically every technical field uses complex numbers, and they appear in many kinds of scientific computation.
What kind of support for complex numbers do we need in a computer language?
A Complex Number Class
To some extent, Fortran taught us the answer long ago: complex numbers in a computer program should behave, to the extent possible, just like any other numbers. That is:
- You should be able to initialize a complex number, assign one complex number to another, and assign a real number to a complex number.
- You should be able to use ordinary mathematical notation for complex arithmetic.
- You should be able to use standard mathematical functions, like sin, cos, exp, and sqrt .
- You should be able to access a complex numbers components, either in (x, y) form or in (r, q) form.
- You should be able to perform I/O with complex numbers just the same way that you do with real numbers.
Fortran achieves these goals by building complex numbers into the language. In C++, its more natural to represent complex numbers as a class, where the real and imaginary parts are member variables. We can overload arithmetic operators to achieve ordinary mathematical notation, and we can overload functions like sin and exp for complex arguments. Finally, since C++s iostream system is a user-extensible library, we can easily support complex I/O: all we have to do is supply the appropriate versions of operator<< and operator>>. To look at it differently, complex was a test of C++s abstraction mechanisms. If you couldnt write a complex number class in C++, it would be a sign that there was something wrong with C++ classes [1].
(As an aside: I meant it when I said that the member variables in a complex number class should be the real and imaginary part. Sometimes books use a complex number class as an example of data abstraction and suggest that internally such a class could use either an (x, y) or an (r, q) representation without affecting any user-visible behavior. Those examples arent usually thought through. Addition of two complex numbers in (r, q) representation is horrendously complicated; in general, theres no better technique than converting them to (x, y) representation, doing the addition, and converting back again. Ive never seen a complete complex number class that uses an (r, q) representation, and I dont expect to.)
Complex number classes have appeared in many C++ books, beginning with the first edition of Bjarne Stroustrups The C++ Programming Language, and pre-standard complex number classes shipped with cfront and other early compilers. The C++ Standard includes a complex number class, but it differs from the pre-standard version in an important way: its a template. That, too, is a lesson from Fortrans complex numbers. In Fortran, you declare single-precision complex numbers to be of type COMPLEX*8 (i.e., twice the size of a single-precision real number) and double-precision complex numbers to be of type COMPLEX*16. C++ has three distinct floating-point types, float, double, and long double; users should have the same choice of precision for complex numbers that they do for ordinary real numbers.
Heres a simple test program, just to show what C++s complex numbers look like:
#include <complex> #include <iostream> int main() { std::complex<double> z1, z2; std::cout << "z1 -> "; std::cin >> z1; std::cout << "z2 -> "; std::cin >> z2; std::complex<double> z3 = z1 * std::exp(z2); std::cout << "Result: " << z3 << std::endl; }There should be no surprises here: were using the same syntax to work with variables of type complex<double> that we would use with double or float. Running the program, we would see something like this:
z1 -> (1, 3) z2 -> (0.5, 0.5) Result: (-0.924428,5.13111)Complex output always looks like this: the number is written as an ordered pair, with the real part followed by the imaginary part. Thats also the usual form for complex input, but, as a special convenience, the library will also accept a number thats written as the real part alone. So, for example, our test program will also accept these inputs:
z1 -> 2 z2 -> (0, 3.14159265) Result: (-2,7.17959e-09)The standard library provides the usual functions to decompose a complex number into its component parts or to build one out of those parts: std::real(z) and std::imag(z) for the real and imaginary components, std::abs(z) and std::arg(z) for the magnitude [2] and angle, std::conj(z) for the complex conjugate [3], and std::polar(r, theta) to create the complex number r eiq. You dont need to use any of these functions to create a complex number given its real and imaginary parts; you just use the classs constructor. For example:
std::complex<float> i(0, 1); std::complex<float>z =std::polar(1, 0.785398163);In addition to these essentially structural functions, the standard library also gives us complex overloads of the mathematical functions defined in <math.h>: exp, log, log10, pow, sqrt, sin, cos, tan, sinh, cosh, and tanh. For the most part there are no mysteries in these functions, but there is one small technical issue. For any complex number z, its easy to see that ez = ez+2ip. Since the natural logarithm is defined as the inverse of the exponential, this presents a problem: when we take the logarithm of a complex number, weve got an infinite number of answers to choose from. When we take the logarithm of -1, should we get ip, or -ip, or 3ip? The choice is essentially a matter of convention; following earlier programming languages, the C++ Standard says that the answer is ip. Or more formally, what it says is that the branch cut for std::log lies on the negative real axis. The choice of branch cut is the same for log10, pow, and sqrt, all of which are similarly multi-valued and all of which can be defined in terms of log, hence the importance of defining the branch cuts the same way for all of them.
The inverse circular and hyperbolic functions, sin-1, cos-1, tan-1, sinh-1, cosh-1, and tanh-1, would present the same issue: they too are multi-valued, so they too require a choice of branch cut to be well-defined except that the issue doesnt arise, because the C++ Standard doesnt include them. Why not? Mainly because, at the time this portion of the C++ Standard was written, Fortran didnt have those functions either. The standardization committee decided (wisely, in my opinion) that this was an area where it was better to be conservative than to be innovative.
Future Evolution
As discussion begins on a future revision of C++, its time to revisit decisions like this: std::complex is useful, but it has some minor annoyances that ought to be corrected.
First, the inverse trigonometric and hyperbolic functions point to an important issue: C compatibility. Standard C++ was designed to be compatible with C which meant C90, since that was the only Standard C that existed at the time. Theres a new C Standard now, though, C99. C99 defines new math functions, including inverse functions like acosh and special functions like erf and gamma. Even more important, C99 includes complex numbers (not as classes, since C99 doesnt have classes, but as new built-in types), and the new C99 math functions are defined for both real and complex arguments. C compatibility is still an important goal; at the least, a future version of C++ probably ought to define the same functions that C does.
Second, while std::complex is integrated into the C++ I/O mechanism, it doesnt make as good a use of that mechanism as it ought to. The built-in types delegate formatting decisions to user-replaceable facets, but std::complex uses a single unmodifiable format. This can interact poorly with internationalization. Consider, for example, the following program, which combines complex output with a European locale:
#include <complex> #include <iostream> #include <locale> int main() { std::locale L("German"); std::cout.imbue(L); std::complex<float> z(1.2, 0.3); std::cout << z << std::endl; }Using the latest version of Microsoft C++, the output is:
(1,2,0,3)This is hardly satisfactory, and theres no good mechanism for users to do anything better.
Finally, while std::complex is a template, its an odd sort of template: its illegal to instantiate it for any types other than float, double, or long double [4]. Theres a reason for that restriction: its not clear how implementors could write a fully general template for, say, std::sin(const std::complex<T>& z). There are a number of possibilities. We might, for example, require complex sin to be implemented in terms of specified real transcendental functions. Or, alternatively, we might allow std::complex<T> to be instantiated for arbitrary types but define transcendental functions only for the three built-in floating-point types. For the C++ Standard, we chose the simplest solution of all, but a future version of C++ may wish to do something more elaborate.
Complex numbers are a well-known example of an abstract data type, and std::complex is one of the simplest classes in the Standard C++ library. Despite that simplicity, std::complex still presents nontrivial design issues. Several of the obvious candidates for library extensions involve std::complex.
Notes
[1] Thats not such a foregone conclusion as it might seem! Java, for example, doesnt satisfy these criteria: it doesnt have operator overloading for user-defined types. Similarly, I found implementing complex numbers in the original version of Eiffel more difficult than I had expected. See my article Implementing a Complex Number Class in Eiffel, Eiffel Outlook, 1995.
[2] Theres also another function, std::norm, which returns the square of the absolute value. The name norm is now somewhat controversial; Im one of the people responsible for it. Im afraid theres no deep significance to the name: we chose it because |z|2 was obviously an important operation, and we simply couldnt think of a better name for it. [How about norml? cda]
[3] The complex conjugate of z is written as
. If, z = x + iy then
= x - iy.
[4] The C++ Standard, §26.2, paragraph 2, says The effect of instantiating the template complex for any type other than float, double, or long double is unspecified.
Matt Austern is the author of Generic Programming and the STL and the chair of the C++ standardization committees library working group. He works at AT&T Labs Research and can be contacted at austern@research.att.com.