Function type signatures for const-qualified, multi-level arrays of arbitrary type (void) in C and C++

Here's the problem in a nutshell

You cannot pass a double ** or a float **, etc. to a function expecting a void const * const *. For C callers, the compiler gives a confusing warning message and for C++ callers an outright error. Callers wind up having to add explicit casts to pass arrays of specific type.

Example

Consider a simple function, max, suitable to be called from both C and C++ callers that computes a maximum value from among several arrays of the same but arbitrary type. For example

typedef enum {DB_FLOAT, DB_DOUBLE} dtype_t;

/* compute max from among several arrays */
double max(void const * const *data, dtype_t t)
{
    /* implementation for finding max for DB_FLOAT and DB_DOUBLE here */
    return 0;
}

int main()
{
    double a1[] = {1.,2.,3.};
    double a2[] = {4.,5.,6.};
    double *arrs[] = {a1, a2};

    double maxval = max(arrs, DB_DOUBLE);

    return 0;
}

Compiling the above program with a C compiler results in the confusing warning message…

foo.c: In function ‘main’:
foo.c:15: warning: passing argument 1 of ‘max’ from incompatible pointer type

whereas compiling the above program with a C++ compiler results in the outright error…
foo.c: In function ‘int main()’:
foo.c:15: error: invalid conversion from ‘double**’ to ‘const void* const*’
foo.c:15: error:   initializing argument 1 of ‘double max(const void* const*, dtype_t)’

References on this issue

The problem is somewhat specific to multiple levels (e.g. more than a single indirection or star*) as well as the use of const qualification for each level.

  • This issue is discussed in greater detail here
  • Another even better discussion is here
  • As well, how C++ handles implicit type conversions is described here

Various solutions for Silo, a C Library

There are several not very nice ways of addressing this issue. Some of the things we considered are outlined below.

For C callers

Replace the function max with a macro of the same name and rename the function. The macro performs an explicit cast on behalf of the caller.
#define max(ARR,DT) _private_max((void const * const *)(ARR), DT)

The problems with this are multiple.
  • It complicates debugging. Caller thinks it is calling a function named max. But, setting a breakpoint in max will fail.
  • It overrides all attempts of the C compiler to perform at least some compile time type checking. The caller could pass any type for the array(s) argument and the compiler would accept it.
  • A macro wrapper is required for each and every function in the API that involves this kind of multi-level array argument. So, it sort of doubles the interface and creates a maintenance problem.

For C++ callers

Take advantage of polymorphism, templates and in-lining (we're going to be doing this in a library header file) to define a type-specific function wrapper.
#ifdef __cplusplus
template <typename ArrType> inline double max(ArrType const * const *arrs, dtype_t t) { return max((void const * const *)arrs, t); }
#endif

The problems with this are also multiple.
  • It requires the use of templated functions which callers may prefer not to use
  • It works only for C++ callers and so must be maintained apart from the C interface
  • Templating makes it convenient to write the wrapper but is way too permissive in that a pointer to pointer to any type will be ok.
We can remove templating and instead write a type-specific wrapper for each array type we intend to support.
#ifdef __cplusplus
inline double max(float const * const *arrs, dtype_t t) { return max((void const * const *)arrs, t); }
inline double max(double const * const *arrs, dtype_t t) { return max((void const * const *)arrs, t); }
#endif
  • This is better in that it will properly allow only those types we want to support.
  • It is worse in that we need a separate inline wrapper for each type. In Silo, there are currently seven primitive types we support and this number has grown on occasion.

The solution we finally arrived at

First, consider this succession of typedefs for single, double and triple level arrays of arbitrary type (Void Constant Pointers (VCP)).

typedef void const *                 DBVCP1_t; /* single level array */
typedef void const * const *         DBVCP2_t; /* double level array */
typedef void const * const * const * DBVCP3_t; /* triple level array */

Instead of defining the types as shown above, we do the following…

typedef void const *                 DBVCP1_t; /* single level array */
typedef void const *                 DBVCP2_t; /* double level array */
typedef void const *                 DBVCP3_t; /* triple level array */

In any Silo API function where the function expects a caller to pass a single-level array (e.g. void const *), we use the type DBCVP1_t. In any Silo API function where the function expects a caller to pass a two-level array (e.g. void const * const *), we use the type DBCVP2_t, etc. However, in the actual function implementation, we cast to the correct type.

double max(DBCVP2_t _data, dtype_t t)
{
    void const * const *data = (void const * const *) _data;

    /* implementation for finding max for DB_FLOAT and DB_DOUBLE here */
    return 0;
}

The value in typedef'ing the type instead of simply using void const * everywhere is to avoid potential confusion for a user who happens to read the library header file for a function prototype. S/he would wind up seeing there a single-level type even for multi-level array arguments causing some confusion. However, the DBCVP2_t eliminates any confusion so long as the reader is curious enough to go find the type definition in the file and read its associated comments.

What about the specific case of arrays of strings (char **)?

In Silo's API, there are a number of functions that take an array of strings, char const * const * as an argument. What happens if a caller passes a char **?

This works fine for C++ callers.

However, C callers will get a type mismatch warning. And, in this case, we have chosen to take no special action in the Silo header file. C callers will be required to explicitly cast to get rid of the warning. For convenience, we define…

typedef char const * const *         DBCAS_t;

in the Silo header file as a synonym for char const * const * to be used in explicit casts.

test_const.tar - example tar file of C/C++ sources to demonstrate the issues (12 KB) Mark Miller, 11/23/2013 01:03 am

Also available in: HTML TXT