Strange warning in a C function const multidimensional-array argument

I'm getting some strange warnings about this code:

typedef double mat4[4][4];

void mprod4(mat4 r, const mat4 a, const mat4 b)
{
/* yes, function is empty */
}

int main()
{
    mat4 mr, ma, mb;
    mprod4(mr, ma, mb);
}

gcc output as follows:

$ gcc -o test test.c
test.c: In function 'main':
test.c:13: warning: passing argument 2 of 'mprod4' from incompatible pointer
type
test.c:4: note: expected 'const double (*)[4]' but argument is of type 'double
(*)[4]'
test.c:13: warning: passing argument 3 of 'mprod4' from incompatible pointer
type
test.c:4:
note: expected 'const double (*)[4]' but argument is of type 'double
(*)[4]'

If I define the function as:

void mprod4(mat4 r, mat4 a, mat4 b)
{
}

Or defining matrices in main as:

mat4 mr;
const mat4 ma;
const mat4 mb;

Or call the function in main as:

mprod4(mr, (const double(*)[4])ma, (const double(*)[4])mb);

Or even defining mat4 as:

typedef double mat4[16];

Makes the warning go away. What is happening here? Am I doing something invalid?

The gcc version is 4.4.3, if relevant.

I also posted on gcc bugzilla: http://gcc.gnu.org/bugzilla/show_bug.cgi?id=47143

My current workaround is making ugly macros that cast stuff for me:

#ifndef _NO_UGLY_MATRIX_MACROS

#define mprod4(r, a, b) mprod4(r, (const double(*)[4])a, (const double(*)[4])b)

#endif

Answer from Joseph S. Myers on gcc bugzilla:

Not a bug. The function parameters are of type "pointer to array[4] of const double" because const on an array type applies to the element type, recursively, and then the outermost array type, only, of a parameter of array type decays to a pointer, and the arguments passed are of type "pointer to array[4] of double" after array-to-pointer decay, and the only case where qualifiers are permitted to be added in assignment, argument passing etc. is qualifiers on the immediate pointer target, not those nested more deeply.

Sounds pretty confusing to me, like the function expects:

pointer to array[4] of const doubles

and we are passing

pointer to const array[4] of doubles

intead.

Or would it be the inverse? The warnings suggest that the function expects a:

const double (*)[4]

which seems to me more like a

pointer to const array[4] of doubles

I'm really confused with this answer. Could somebody who understands what he said clarify and exemplify?


Solution 1:

I believe the problem is the constraints specified in C99 6.5.16.1(1), which seem to prohibit mixing qualifications in assignments, except for pointers for which an inclusive-qualifier exception is defined. The problem is that with indirect pointers, you end up passing a pointer to one thing to a pointer to another. The assignment isn't valid because, if it was, you could fool it into modifying a const-qualified object with the following code:

const char **cpp;
char *p;
const char c = 'A';
cpp = &p;  // constraint violation
*cpp = &c; // valid
*p = 0;    // valid by itself, but would clobber c

It might seem reasonable that cpp, which promises not to modify any chars, might be assigned a pointer to an object pointing at non-qualified chars. After all, that's allowed for single-indirect pointers, which is why, e.g., you can pass a mutable object to the second parameter of strcpy(3), the first parameter to strchr(3), and many other parameters that are declared with const.

But with the indirect pointer, at the next level, assignment from a qualified pointer is allowed, and now a perfectly unqualified pointer assignment will clobber a qualified object.

I don't immediately see how a 2-D array could lead to this situation, but in any case it hits the same constraint in the standard.

Since in your case, you aren't actually tricking it into clobbering a const, the right thing for your code would seem to be inserting the cast.


Update: OK guys, as it happens this issue is in the C faq, and this entire discussion has also taken place several times on the gcc bug list and on the gcc mailing list.

  • Gcc bug list: http://gcc.gnu.org/bugzilla/show_bug.cgi?id=20230.
  • C FAQ: it's question 11.10: http://c-faq.com/ansi/constmismatch.html

The lesson: you can pass a T *x when const T *x is expected, by explicit exception, but T *x and const T *x are still distinct types, so you can't pass a pointer to either one to a pointer to the other.

Solution 2:

To explain what Joseph said: the function is expecting a pointer to array[4] of const double to be passed in, but you're passing in a pointer to array[4] of double. These types are not compatible, so you get an error. They look like they should be compatible, but they're not.

For the purposes of passing parameters to functions (or for variable assignments), you can always convert an X to a const X, or a pointer to X to a pointer to const X for any type X. For example:

int x1 = 0;
const int x2 = x1;  // ok
int *x3 = &x1;
const int *x4 = x3;  // ok: convert "pointer to int" to "pointer to const int"
int **x5 = &x3;
const int **x6 = x5;  // ERROR: see DigitalRoss's answer
int *const *x7 = x5;  // ok: convert "pointer to (pointer to int)" to
                      //             "pointer to const (pointer to int)"

You're only allowed to add qualifiers (that is, the const, volatile, and restrict qualifiers) to the first level of pointers. You can't add them to higher levels of pointers because, as DigitalRoss mentioned, doing so would allow you to accidentally violate const-correctness. This is what Joseph means by "the only case where qualifiers are permitted to be added in assignment, argument passing etc. is qualifiers on the immediate pointer target, not those nested more deeply."

So, bringing us back to Joseph's response, you can't convert a pointer to array[4] of double to a pointer to array[4] of const double because there is no type X such that you're converting from pointer to X to pointer to const X.

If you try using array[4] of double for X, you'd see that you can convert to pointer to const array[4] of double, which is a different type. However, no such type exists in C: you can have an array of a const type, but there is no such thing as a const array.

Hence, there's no way to perfectly solve your problem. You'll have to either add casts to all of your function calls (either manually or via a macro or helper function), rewrite your functions to not take const parameters (bad since it doesn't let you pass in const matrices), or change the mat4 type to be either a 1-dimensional array or a structure, as user502515 suggested.

Solution 3:

To practically solve this, one could use a struct, and the change of double[4][4] into the a-bit-awkward double (*)[4] is avoided, and constness also works intuitively — while the same amount of memory is used:

struct mat4 {
        double m[4][4];
};

void myfunc(struct mat4 *r, const struct mat4 *a, const struct mat4 *b)
{
}

int main(void)
{
        struct mat4 mr, ma, mb;
        myfunc(&mr, &ma, &mb);
}

Solution 4:

I think in C99, you can do this, but I'm not sure it will help:

void mprod4(double mr[4][4], double ma[const 4][const 4], double mb[const 4][const 4])
{

}

I haven't got a C99 compiler handy but I remember reading something in the C99 specification regarding qualifiers within the [] for arrays as arguments. You can also put static in there (e.g. ma[static 4]) but of course that means something else.

Edit

Here it is, section 6.7.3.5 paragraph 7.

A declaration of a parameter as “array of type” shall be adjusted to “qualified pointer to type”, where the type qualifiers (if any) are those specified within the [ and ] of the array type derivation. If the keyword static also appears within the [ and ] of the array type derivation, then for each call to the function, the value of the corresponding actual argument shall provide access to the first element of an array with at least as many elements as specified by the size expression.

Solution 5:

Here's a problem (IMHO): double[4][4] in a function signature.

You know it's a double[4][4], but the compiler sees double(*)[4] in the function paramter list, which notably has no array size constraint. It turns your 2D array of 4 by 4 objects into a pointer to a 1D array of 4 objects, and the pointer can be validly indexed as if it were an array of 4 objects.

I would pass all mat4 objects by pointer:

void mprod4(mat4 *r, const mat4 *a, const mat4 *b);
// and, if you don't want to hairy your syntax
#define mprod4(r, a, b) (mprod4)(&r, (const mat4 *)&a, (const mat4 *)&b)

This will (I believe) ensure const correctness and array size correctness. It may make mprod4 a bit harder to write, and still involves some hairy casts, but it'll (IMHO) be worth it (especially after the macro above):

void mprod4(mat4 *r, const mat4 *a, const mat4 *b)
{
    // all indexing of the matricies must be done after dereference
    for(int i = 0; i < 4; i++) for(int j = 0; j < 4; j++)
      {
        (*r)[i][j] = (*a)[i][j] * (*b)[i][j];
        // you could make it easier on yourself if you like:
        #define idx(a, i, j) ((*a)[i][j])
        idx(r, i, j) = idx(a, i, j) * idx(b, i, j)
      }
}

It may look a bit bad when you write it, but I think it'll be cleaner type-wise. (Maybe I've been thinking C++ too much...)