Proper C code design that handles both floating point with one and two points?

I am developing a library of special mathematical functions in C. I need to provide the library with the ability to handle both single-point and double precision. The important point here is that the "single" functions should use ONLY the "single" arithmetic inside (respectively, for the "double" functions).

As an illustration, consider LAPACK (Fortran), which provides two versions of each of its functions (SINGLE and DOUBLE). In addition, a C-math library (example, expf and exp ).

To clarify, I want to support something similar to the following (far-fetched) example:

float MyFloatFunc(float x) { return expf(-2.0f * x)*logf(2.75f*x); } double MyDoubleFunc(double x) { return exp(-2.0 * x)*log(2.75*x); } 

I thought of the following approaches:

  • Using macros for the function name. This still requires two separate source codebases:

     #ifdef USE_FLOAT #define MYFUNC MyFloatFunc #else #define MYFUNC MyDoubleFunc #endif 
  • Using macros for floating point types. This allows me to share the codebase between two different versions:

     #ifdef USE_FLOAT #define NUMBER float #else #define NUMBER double #endif 
  • Just design two separate libraries and forget about trying to save headaches.

Does anyone have any recommendations or additional suggestions?

+8
c floating-point
source share
4 answers

For polynomial approximations, interpolations, and other inherently approximate mathematical functions, you cannot share the code between double precision and implementation with one precision, without wasting time on the single-point version or be more approximate than necessary in the double-resonance version.

However, if you are following the path of a single code base, the following should work for constants and standard library functions:

 #ifdef USE_FLOAT #define C(x) x##f #else #define C(x) x #endif ... C(2.0) ... C(sin) ... 
+7
source share

(Partly inspired by Pascal Quoc's answer) If you need one library with floating and double versions of everything, you can use the recursive #include in combination with macros. This does not lead to the clearest of the code, but allows you to use the same code for both versions, and obfuscation is pretty subtle, probably manageable:

mylib.h:

 #ifndef MYLIB_H_GUARD #ifdef MYLIB_H_PASS2 #define MYLIB_H_GUARD 1 #undef C #undef FLT #define C(X) X #define FLT double #else /* any #include needed in the header go here */ #undef C #undef FLT #define C(X) X##f #define FLT float #endif /* All the dual-version stuff goes here */ FLT C(MyFunc)(FLT x); #ifndef MYLIB_H_PASS2 /* prepare 2nd pass (for 'double' version) */ #define MYLIB_H_PASS2 1 #include "mylib.h" #endif #endif /* guard */ 

mylib.c:

 #ifdef MYLIB_C_PASS2 #undef C #undef FLT #define C(X) X #define FLT double #else #include "mylib.h" /* other #include */ #undef C #undef FLT #define C(X) X##f #define FLT float #endif /* All the dual-version stuff goes here */ FLT C(MyFunc)(FLT x) { return C(exp)(C(-2.0) * x) * C(log)(C(2.75) * x); } #ifndef MYLIB_C_PASS2 /* prepare 2nd pass (for 'double' version) */ #define MYLIB_C_PASS2 1 #include "mylib.c" #endif 

Each #include file itself one more time, using different macro definitions in the second pass, to generate two versions of the code that uses macros.

Some people may object to this approach.

+5
source share

The big question for you will be:

  • Is it easier to maintain two separate inexperienced source trees or one tangled one?

If you have the proposed general coding, you will have to write restrained code, being very careful not to write any unordered constants or calls to non-macro functions (or function bodies).

If you have separate source code trees, it will be easier to maintain the code in that each tree will look like regular (not obfuscated) C-code, but if YourFunctionA has an error in the "float" version, you always remember to make the corresponding change in the "double" version.

I think it depends on the complexity and volatility of the functions. My suspicion is that once written and debugged for the first time, there will rarely be a need to return to it. This actually means that it doesn’t matter which mechanism you use - both will work. If the bodies of the functions are somewhat volatile or the list of functions is volatile, then a single code base can be simpler overall. If everything is very stable, the clarity of two separate code bases may make this preferable. But it is very subjective.

I would probably go with a single code base and wall-to-wall macros. But I'm not sure which is better, and the other way also has its advantages.

+2
source share

The <tgmath.h> header, standardized in C 1999, provides typical calls for routines in <math.h> and <complex.h>. After you enable <tgmath.h>, the source text "sin (x)" will call sinl if x is a long double, sin if x is a double, and sinf if x is a float.

You will still need to conditionally adjust your constants so that you use "3.1" or "3.1f" depending on the situation. There are many syntactic methods for this, depending on your needs and what seems more aesthetic to you. For constants that are precisely represented in exactly float, you can simply use the float form. For example, "y = .5f * x" will automatically convert .5f to .5 if x is double. However, "sin (.5f)" will produce sinf (.5f), which is less accurate than sin (.5f).

Perhaps you can reduce convention to one clear definition:

 #if defined USE_FLOAT typedef float Float; #else typedef double Float; #endif 

Then you can use constants this way:

 const Float pi = 3.14159265358979323846233; Float y = sin(pi*x); Float z = (Float) 2.71828182844 * x; 

This may not be entirely satisfactory because there are rare cases when a number converted to double and then to float is less accurate than a number converted directly to float. That way, you might be better off with the macro described above, where β€œC (numeral)” appends the suffix to the number if necessary.

+1
source share

All Articles