Document number: N1589=04-0029
 
Howard E. Hinnant, hinnant@twcny.rr.com
 
February 11, 2004

complex and Issue 387

I fear that this is a bicycle shed issue. However I signed up in Kona to attempt to resolve this issue and this document reflects my commitment.

LWG issue 387 was heavily discussed on the lib reflector from Jan. 10 to Feb. 6, 2004. Many suggestions were made. In this paper I attempt to collect and present the various suggestions. No conclusions are reached nor recommendations made. It is hoped that an orderly presentation of the different suggestions will aid discussion and resolution. Apologies in advance for any suggestions which I may have inadvertently mangled, or accidentally omitted altogether.

Problem statement

There are two problems 387 is attempting to address:

  1. Make complex<T> layout compatible with the complex data types of C and Fortran.
  2. Enable efficient write access to the individual parts (real and imaginary) of the complex type.

To the best of my knowledge, all existing implementations already are layout compatible with C and Fortran. That is, the real part is stored first, the imaginary part second, and the real and imaginary parts occupy contiguous storage:

sizeof(complex<T>) == 2 * sizeof(T)
&imag(z) == &real(z) + 1

Note: the above is not legal C++ code, it is just meant as a concise statement of the layout requirements.

And so the first goal of 387 does not appear to be controversial, and that part of the proposed resolution can simply be accepted.

The second goal (write access to the data) seems to be where the controversy is centered. Note that once the layout is standardized, write access to the data members is granted, and it is just a matter of what syntax the client is allowed to use in order to achieve access.

There are (at least) two motivating use cases for write access to the data members:

  1. Passing a vector<complex<double> > to legacy C or Fortran code.
  2. Assigning a value to one part of a complex without changing the other part.

Options

I will attempt to enumerate all of the options I have seen to date, and show their syntax for the two motivating examples mentioned above. The options are labeled 1-7:

  1. No change to the current interface (other than the layout guarantee).
  2. Return references from real() and imag() (as proposed in 387).
  3. Add a data() member function which returns a T* to the real part.
  4. Add a complex_cast function taking a complex<T>* and returning a T*.
  5. Add an operator[](size_t) which returns a reference to the data.
  6. Expose the data publicly as an array, say: T data[2];
  7. Get/Set functions, perhaps named real(T), imag(T).

Example 1

Given:

void foo(double*, size_t);
std::vector<std::complex<double> > v;
...

And given that the vector is not empty, here is the syntax a client might use for passing the vector to foo:

  1. foo(reinterpret_cast<double*>(&v[0]), 2*v.size());
  2. foo(&v[0].real(), 2*v.size());
  3. foo(v[0].data(), 2*v.size());
  4. foo(complex_cast(&v[0]), 2*v.size());
  5. foo(&v[0][0], 2*v.size());
  6. foo(&v[0].data[0], 2*v.size());
  7. foo(reinterpret_cast<double*>(&v[0]), 2*v.size());

If a data() member function were to be added to vector then zero-sized vectors could be handled (without an explicit test) with:

foo(reinterpret_cast<double*>(v.data()), 2*v.size());
foo(complex_cast(v.data()), 2*v.size());

Example 2

Given:

std::complex<double> z;

Here is how a client might assign 1.0 to the imaginary part of z:

  1. z = std::complex<double>(z.real(), 1.0);
  2. z.imag() = 1.0;
  3. *(z.data() + 1) = 1.0;
  4. *(complex_cast(&z) + 1) = 1.0;
  5. z[1] = 1.0;
  6. z.data[1] = 1.0;
  7. z.imag(1.0);

A final note: We should realize that these suggestions are not exclusive. For example we could adopt both 2 (real() returns a reference) and 4 (complex_cast).