Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better C++ support (#254) #255

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open

Better C++ support (#254) #255

wants to merge 1 commit into from

Conversation

maazl
Copy link

@maazl maazl commented Aug 31, 2021

The patch below allows to use fftw3.h with native support for C++ std::complex.

Since it is not possible in C++ to detect automatically whether the header <complex> has been included this is an opt in feature. The macro FFTW_cpp_complex has to be defined:

#define FFTW_cpp_complex
#include "fftw3.h"

If this is done in C++ context and the C++ version is at least C++11 then the fftw*_complex types are replaced by the class std::complex<R>.

Optionally the users may define their own implementation of FFTW_DEFINE_COMPLEX(R, C). This takes precedence over the implementation in fftw3.h. But whether this should be a documented property is questionable since wired things happen if the type is not binary compatible.

@@ -50,16 +50,22 @@
#include <stdio.h>

#ifdef __cplusplus
# if __cplusplus >= 201103L && defined(FFTW_cpp_complex)
# include <complex>
# define FFTW_DEFINE_COMPLEX(R, C) typedef ::std::complex<R> C
Copy link
Contributor

@stevengj stevengj Feb 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naive question: What is the difference between ::std::complex and std::complex here?

@stevengj
Copy link
Contributor

stevengj commented Feb 9, 2022

This seems fine to me. Needs a documentation patch here:

fftw3/doc/reference.texi

Lines 66 to 80 in 7188a4c

C++ has its own @code{complex<T>} template class, defined in the
standard @code{<complex>} header file. Reportedly, the C++ standards
committee has recently agreed to mandate that the storage format used
for this type be binary-compatible with the C99 type, i.e. an array
@code{T[2]} with consecutive real @code{[0]} and imaginary @code{[1]}
parts. (See report
@uref{http://www.open-std.org/jtc1/sc22/WG21/docs/papers/2002/n1388.pdf
WG21/N1388}.) Although not part of the official standard as of this
writing, the proposal stated that: ``This solution has been tested with
all current major implementations of the standard library and shown to
be working.'' To the extent that this is true, if you have a variable
@code{complex<double> *x}, you can pass it directly to FFTW via
@code{reinterpret_cast<fftw_complex*>(x)}.
@cindex C++
@cindex portability

Just add something like "Alternatively, if you #define FFTW_cpp_complex before including fftw3.h, then FFTW will use std::complex for its complex-number types in the API. (This does not not require recompiling FFTW because of the above-mentioned binary compatibility.)"

@stevengj
Copy link
Contributor

stevengj commented Feb 9, 2022

On a separate note, it would be good to update that section of the manual to say what version of the C++ standard eventually adopted that proposal. (2002 is no longer "recent"!)

@@ -50,16 +50,22 @@
#include <stdio.h>

#ifdef __cplusplus
# if __cplusplus >= 201103L && defined(FFTW_cpp_complex)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add these to cmake (and autotools)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You wouldn't need them for compiling FFTW itself…

Copy link

@LecrisUT LecrisUT Jan 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, good point. But still they need to be included in cmake export and pkg-config. Or at least documented somewhere

Edit: This could look like:

target_compile_definitions(${fttw_libs} $<$<AND:INSTALL_INTERFACE,CMAKE_CXX_COMPILER>:FFTW_cpp_complex>)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They wouldn't be in pkg-config etcetera because you would need to enable this API in manually user code — it can't be turned on automatically, since that would break backwards compatibility. Definitely would need docs, however.

@matteo-frigo
Copy link
Member

I want to push back against this whole idea, and I say this as the dude who had the brilliant idea of redefining fftw_complex to be double _Complex if <complex.h> was included in a C program. (I now regard this idea as a mistake, which thankfully the world has ignored for the most part.)

The main problem with this approach is that it is not composable. (This is true both in C and C++.) A typical C++ source file in 2023 will include a thousand or so files, directly or indirectly. If any two of them have a different idea of what fftw_complex means, one change to a header file will break another unrelated header file that is included later.

The second issue is that this change does not really solve any problems. A typical C++ program will have a wrapper that converts whatever std::vector<std::complex> thingy the user may need to a call to fftw_foo(). This change only "solves" the inner part (std::complex vs. fftw_complex), but doesn't address the fact that the user really wants a vector and not a C-style array, or that the user is using the code in a template that doesn't expect to have to use different function names for every precision. A real C++ program will also want to control the lifetime of plans via constructors/destructors/move semantics/exception safety, etc. So the user has to write the wrapper anyway.

The third issue is that this change violates the C++ one-definition rule (I think).

The reality is that FFTW is a proud old-school C program and it is pointless to pretend that it is a C++ program.

Now, if somebody volunteers to develop a proper C++ API that conforms to the typical best practices of that language in 2023 so that new programs can have an easier life, I am open to having that conversation. The solution will have to look like a new header file (or something), supported by bunch of C or C++ files in a new directory api++/, and must be a purely additive contribution that does not affect existing code at all.

@LecrisUT
Copy link

LecrisUT commented Jan 2, 2023

About the C++ api implementation. It is possible to translate the macros to a combination of templates, traits and static_cast, but the only issue is that we would need to change all the way down where the macro X is defined. It might be possible to use extern, define some simple wrapper functions around it for each case and not include the C api at all, but we need to be careful to avoid compiler name (de)mangling.

@stevengj
Copy link
Contributor

stevengj commented Jan 2, 2023

Regarding C++ apis, see also FFTW++ / github fftwpp

@maazl
Copy link
Author

maazl commented Jan 3, 2023

The main problem with this approach is that it is not composable. (This is true both in C and C++.) A typical C++ source file in 2023 will include a thousand or so files, directly or indirectly. If any two of them have a different idea of what fftw_complex means, one change to a header file will break another unrelated header file that is included later.

This argument applies to almost any #define in configuration headers.

The second issue is that this change does not really solve any problems. A typical C++ program will have a wrapper that converts whatever std::vectorstd::complex thingy the user may need to a call to fftw_foo(). This change only "solves" the inner part (std::complex vs. fftw_complex), but doesn't address the fact that the user really wants a vector and not a C-style array, or that the user is using the code in a template that doesn't expect to have to use different function names for every precision. A real C++ program will also want to control the lifetime of plans via constructors/destructors/move semantics/exception safety, etc. So the user has to write the wrapper anyway.

In fact I see no need to wrap the entire FFTW API (like fftw++ does).

Object lifetime can easily be managed by the standard smart pointers, e.g. std::unique_ptr<fftw_plan> with fftwf_destroy_plan as deleter.
And the standard C++ containers could just use an FFTW allocator to ensure alignment. Applications with very large transforms might even allocate large blocks directly from the kernel or MMU.
And if FFTW would expose the required alignment as compile time constant the C++ feature alignas might use the probably more effective allocator of the C++ runtime.

All of these options become more complicated with a full C++ wrapper that also covers the array types.

The internal storage of std::vector<T> is guaranteed to be binary compatible to a C array pointer T*. Applications with a constant plan size might even prefer a fixed size std::array. But this won't help when the type T is incompatible. A cast is required for any invocation of an API function with a complex array parameter.

The third issue is that this change violates the C++ one-definition rule (I think).

... like any other configuration #define. It is up to the programmer to ensure correct usage. Such settings should be part of the global configuration headers.

In fact a different type could be intended too. Legacy code may be partially C and C++. In this case the C parts need another type than the C++ parts.

The reality is that FFTW is a proud old-school C program and it is pointless to pretend that it is a C++ program.

Now, if somebody volunteers to develop a proper C++ API that conforms to the typical best practices of that language in 2023 so that new programs can have an easier life, I am open to having that conversation. The solution will have to look like a new header file (or something), supported by bunch of C or C++ files in a new directory api++/, and must be a purely additive contribution that does not affect existing code at all.

The basic idea of this patch is to allow a lightweight integration of FFTW into C++ code. This also simplifies the migration of old C legacy code to C++ w/o rewriting almost every line.

@stevengj
Copy link
Contributor

stevengj commented Jan 3, 2023

This argument applies to almost any #define in configuration headers.

Only to #define statements controlled by the user, i.e. in the caller's code. (As opposed to configuration headers that are created when a library is built and are installed with the library header. But FFTW doesn't use those either.)

All of these options become more complicated with a full C++ wrapper that also covers the array types.

Yes, as C++ keeps evolving, the notion of what constitutes a "proper C++" wrapper has changed significantly over the years. But it still seems like C++ should ideally have its own header file.

@matteo-frigo
Copy link
Member

@maazl Believe me, I am sympathetic to most of your points, and 20 years ago I would have happily agreed with you.

But we went down this path before, and we have come to regret it. For example, FFTW_NO_Complex exists because somebody had something like

#include "foo.h"
#include "fftw3.h"

where foo.h included <complex.h>, thus changing the meaning of fftw_complex in fftw3.h. This was in a simpler old-school world where people used dependencies sparingly, and where the C language does not encourage the needless proliferation of data types.

A typical C++ program these days has thousands of header files included multiple times. There is a whole industry of tools and so-called "best practices" devoted to "managing dependencies", with tools that reorder the inclusion order of header files depending on some set of conventions. It is expected that whatever program one writes should be usable as a subroutine of some other program.

In this world, you won't be able to write file A.h like this:

A.h:
#define FFTW_cpp_complex
#include <fftw3.h>

because some other file B.h, which uses the C types, may include C.h which includes A.h. So in practice people won't use the solution that you are suggesting, and we'll have wasted everybody's time documenting why they shouldn't use the feature that we provide.

@maazl
Copy link
Author

maazl commented Jan 3, 2023

@matteo-frigo
Maybe another option would be to add overloads to the functions that take a pointer to std::complex* and forward the calls inlíne to the original functions. This is not intrusive and need not be part of fftw3.h at all.
I did not test whether this works in all cases. The FFTW application where I got the issue is no longer affected because it switched to half complex data structures for other reasons.

@leekillough
Copy link

As a long-time math library and C++ expert, here's my 2 cents:

I don't think a C library like FFTW needs C++ to be added to its main code.

A separate fftw.hpp header could be added with inline and/or template definitions as wrappers, but it should not require changes to core FFTW unless absolutely necessary, and then only with #if / #ifdef guards. You must also consider which version of C++ to support -- C++98, C++03, C++11, C++14, C++17, C++20, C++23 (I would go with C++11).

_Complex was made optional in C11, after it was mandatory in C99. C++ std::complex is a representation-compatible but distinct type from C _Complex (or complex macro in <complex.h>). All of these complexities make it hard to support mixing C and C++ in a mature library like FFTW.

If you want to pass a C++ std::vector<std::complex<double>> to FFTW, you can use something like (not tested):

std::vector<std::complex<double>> vx;
fftw_complex* vx_fftw = reinterpret_cast<fftw_complex*>(&vx[0]);

... except that it technically violates strict aliasing (should not matter in practice). You could also use std::vector<fftw_complex>, and then &vx[0] would require no cast.

If you want to change FFTW, it should be in the form of the addition of a C++ header file like fftw.hpp -- no .cpp files, to avoid introducing the dependency on a C++ compiler for FFTW builds -- and it should provide wrappers to the C functions. If you want an OOP RAII model, you could even define C++ types whose constructors and destructors call the FFTW C functions to allocate and deallocate memory. inline and template should be used to avoid violating ODR.

There's also fftwpp, as mentioned earlier, as well as fftw_cpp. More examples are listed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants