bremen_matt
bremen_matt

Reputation: 7349

Passing array to a function (and why it does not work in C++)

I have come across some C code that compiles, but I do not understand why. Specifically, I have a C library that has a lot of code using this format:

void get_xu_col(int i_start,
                int n,
                double x[n],
                int n_x,
                int n_u,
                int n_col,
                double xu_col[n_col][n_x + n_u]){
    ... 
}

int main(){
    ...
    double xu_col[n_col][n_x + n_u];
    get_xu_col( ..., xu_col );
    ...
}

What I don't understand is why the compiler allows for sizing in the arrays. To the best of my understanding, either the sizes must be fixed (e.g. xu_col[9][7]) or undefined (e.g. xu_col[][]). In the above code, it appears that the sizes are not compile-time constants.

Is the compiler just ignoring the arguments here? or is it really doing a compile-time check on the dimensions?

If it is the latter, then it seems error-prone to pass the dimensions separately.

The second part of the question is:

Why doesn't the same version work in C++? When I literally change the file extension from .c to .cpp and try to recompile, I get

candidate function not viable: no known conversion from 'double [n_col][n_x + n_u]' to 'double (*)[n_x + n_u]' for 7th argument
void get_xu_col(int i_start, int n, double x[n], int n_x, int n_u, int n_col, double xu_col[n_col][n_x + n_u]);

I would like to know which idiom I should use to convert this code to C++, since apparently the previous idiom was something that works in C, but not C++.

Upvotes: 45

Views: 9595

Answers (7)

user3629249
user3629249

Reputation: 16540

regarding the second part of your question:

Why doesn't the same version work in C++? When I literally change the file extension from .c to .cpp and try to recompile, I get

The source of that problem is that C++ mangles names.

To avoid name mangling when running C++ and trying to access a C library.

near the top of the header file for the C library, after the multiple inclusion guard insert:

#ifdef __cplusplus
extern "C" {
#endif

and near the end of the header file, before the #endif of the multiple inclusion guard, insert:

#ifdef __cplusplus
}
#endif

That will eliminate the problem of functions in the assocated library file not being found

Upvotes: 0

Davislor
Davislor

Reputation: 15144

The difficulty with your code sample is that one of the function parameters is protoyped, double xu_col[n_col][n_x + n_u], where n_x and n_u are variables, not constants. If you just pass this as a double[] instead, some C++ compilers might allow a cast such as double (&table)[n_col][n_x + n_u] = (double(&)[n_col][n_x + n_u])xu_col; to work as a non-standard extension, but the portable approach would be to write accesses like xu_col[i*(n_x+n_u) + j], which you could simplify with a helper function if that’s too ugly.

An alternative approach, probably more in keeping with the spirit of the STL, might be to write a minimal container class that knows its dimensions, stores elements in a linear array for efficiency. Then you might declare redim_array<double> table = redim_array<double>(xu_col, n_col*(n_x+n_u)).redim(n_col, n_x+n_u); and access table(i,j).

Several of the other answers have described the syntax of variable-length arrays, but another aspect of your question is how it’s legal to implicitly convert a rectangular¹ two-dimensional array to a one-dimensional array.

What happens is that the rectangular array is laid out as consecutive elements in memory, so it can degenerate to a pointer to the elements, and then the function parameter can interpret that as an array with a different geometry.

Here’s a short little program that demonstrates this behavior.

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

#define ROWS 2
#define COLS 4
#define ELEMS (ROWS*COLS)

int flatten_array( const ptrdiff_t n, const int a[n] )
{
  int printed = 0;

  for ( ptrdiff_t i = 0; i < n; ++i )
    printed += printf( "%d ", a[i] );

  return printed + printf("\n");
}

int rectangular_array( const ptrdiff_t m,
                       const ptrdiff_t n,
                       const int a[m][n] )
{
  int printed = 0;

  for ( ptrdiff_t i = 0; i < m; ++i ) {
    for ( ptrdiff_t j = 0; j < n; ++j )
      printed += printf( "%d ", a[i][j] );

    printed += printf("\n");
  }

  return printed + printf("\n");
}

int main(void)
{
  static const int matrix[ROWS][COLS] = {
    {11, 12, 13, 14},
    {21, 22, 23, 24}
  };
  static const int vector[ELEMS] = {11, 12, 13, 14, 21, 22, 23, 24};

  flatten_array( ELEMS, *(const int (*const)[ELEMS])matrix );
  printf("\n");
  rectangular_array( ROWS, COLS, *(const int (*const)[ROWS][COLS])vector );

  return EXIT_SUCCESS;
}

There’s some language-lawyering in the comments below² about whether passing the array arguments without the explicit casts is technically legal by standard. I’ve chosen to relegate that to a footnote and just delete the example with no casts. In the real world, you will sometimes see code without the pointer-to-array-of-different-geometry cast, and it might generate a warning. The memory layout of the two arrays is required by the standard to be the same.

To convert to C++, you can use the pointer-conversion trick, or you now can code-golf it a bit by using references.

Here is a C++ translation of the program above. It requires all but the first dimension of the array being passed in to be constexpr, but some compilers support C99-style variable-length arrays as an extension.

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

constexpr ptrdiff_t rows = 2;
constexpr ptrdiff_t cols = 4;
constexpr ptrdiff_t elems = rows * cols;

int flatten_array( const ptrdiff_t n, const int a[] )
{
  int printed = 0;

  for ( ptrdiff_t i = 0; i < n; ++i )
    printed += printf( "%d ", a[i] );

  return printed + printf("\n");
}

int rectangular_array( const ptrdiff_t n, const int a[][cols] )
{
  int printed = 0;

  for ( ptrdiff_t i = 0; i < n; ++i ) {
    for ( ptrdiff_t j = 0; j < cols; ++j )
      printed += printf( "%d ", a[i][j] );

    printed += printf("\n");
  }

  return printed + printf("\n");
}

int main(void)
{
  static const int matrix[rows][cols] = {
    {11, 12, 13, 14},
    {21, 22, 23, 24}
  };
  static const int vector[elems] = {11, 12, 13, 14, 21, 22, 23, 24};

  flatten_array( elems, (const int(&)[elems])matrix );
  printf("\n");
  rectangular_array( rows, (const int(&)[rows][cols])vector );

  return EXIT_SUCCESS;
}

¹ C programmers sometimes call either arrays like int matrix[ROWS][COLS] or arrays like char** argv “two-dimensional arrays.” Here, I call the former rectangular and the latter ragged.

² The constraint on function arguments in the C11 standard is ‘Each argument shall have a type such that its value may be assigned to an object with the unqualified version of the type of its corresponding parameter.’ Furthermore ‘A declaration of a parameter as ''array of type'' shall be adjusted to ''qualified pointer to type''’ and, if this applies recursively, a multidimensional array of some type will be adjusted to a flat pointer of that type.

Upvotes: 4

supercat
supercat

Reputation: 81159

When a parameter is declared as having a single-dimensional array type, C ignores the given size and instead treats the parameter as a pointer to the element type. For nested (multi-dimensional) arrays, such treatment is only applied to the outer array. In C89, inner dimensions had to have fixed sizes, but in C99 the dimensions can be expressions. If parameters which are needed to compute an array's size are not listed until after the array, it will be necessary to use a curious mixture of old and new syntax to declare the function, e.g.

int findNonzero(short dat[*][*], int rows, int cols);
int findNonzero(dat, rows, cols)
    int rows,cols;
    short dat[static rows][cols];
{
    for (int i=0; i<rows; i++)
        for (int j=0; j<cols; j++)
            if (dat[i][j] != 0) return i;
    return -1;
}

Note that the array sizes are specified as * in the function prototype, and that the function definition does not specify types in the argument list but instead describes all the parameters' types between the argument list and the opening brace. Note also that while the compiler is likely to ignore the number of rows in the array declaration, but a smart compiler may be able to use it to facilitate optimization. Effectively, the weird "static" syntax invites the compiler to read any parts of the array, up to the given size, as it sees fit, whether or not the values are read by the code. This may be helpful on some platforms where code might benefit from processing multiple items of the array at once.

Upvotes: 5

Jens Harms
Jens Harms

Reputation: 436

What I don't understand is why the compiler allows for sizing in the arrays. To the best of my understanding, either the sizes must be fixed (e.g. xu_col[9][7]) or undefined (e.g. xu_col[][]). In the above code, it appears that the sizes are not compile-time constants.

You are right, the sizes are not compile-time constants. If you have a two-dimensional array, x[line][col] the compiler needs the number of elements in a line to calculate the address of an element. Look at get_char_2() and get_char_3() example code.

If you use variable length arrays (VLAs) as function parameters you have to supply these number (see get_char_1 example). you can write:

 my_func( x[][width] )

or you can write

 my_func( x[999][width] )

Is the compiler just ignoring the arguments here? or is it really doing a >compile-time check on the dimensions?

The first number (999) will be ignored by the compiler. The second is needed. Without the line size, the compiler can not calculate addresses inside these 2D-array. The compiler does not do run-time or compile-time checks for VLAs in C.

/* file: vla.c
 *
 * variable length array example
 *
 * compile with:
 *   
 *    gcc -g -Wall -o vla vla.c 
 *
 */

#include <stdio.h>
#include <wchar.h>


/* 4 Lines - each line has 8 wide-characters */
wchar_t tab[][8] = {
{ L"12345678" },
{ L"abcdefgh" },
{ L"ijklmnop" },
{ L"qrstuvwx" }
};

/* memory layout:   
   0x00:   0x0031  0x0032 0x0033  0x0034  0x0035  0x0036  0x0037  0x0038 
   0x20:   0x0061  0x0062 0x0063  0x0064  0x0065  0x0066  0x0067  0x0068 
   ...

*/



/* get character from table w/o variable length array and w/o type */
char get_char_3(int line, int col, int width, int typesize, void *ptr )
{
char ch = * (char *) (ptr + width * typesize * line + col * typesize ); 

printf("line:%d col:%d char:%c\n", line, col, ch ); 
return ch;
}


/* get character from table w/o variable length array */
char get_char_2(int line, int col, int width, wchar_t *ptr)
{
char ch = (char) (ptr + width * line)[col]; 

printf("line:%d col:%d char:%c\n", line, col, ch ); 
return ch;
}

/* get character from table : compiler does not know line length for 
   address calculation until you supply it (width). 
*/
char get_char_1(int line, int col, int width, wchar_t aptr[][width] )
{
/* run-time calculation: 
   (width * sizeof(char) * line)  + col 
     ???    KNOWN          KOWN     KNOWN
*/
char ch = (char) aptr[line][col];

printf("line:%d col:%d char:%c\n", line, col, ch ); 
return ch;
}


int main(void)
{
char ch;

ch = tab[1][7]; /* compiler knows line length */
printf("at 1,7 we have: %c\n",  ch );

/* sizeof tab[0][0] == sizeof(wchar_t) */ 

ch = get_char_1(1,7, sizeof(tab[0])/sizeof(tab[0][0]), tab);
printf("1 returned char: %c\n", ch );

ch = get_char_2(1,7, sizeof(tab[0])/sizeof(tab[0][0]), (wchar_t*)tab);
printf("2 returned char: %c\n", ch );

ch = get_char_3(1,7, sizeof(tab[0])/sizeof(tab[0][0]),
        sizeof( wchar_t), tab);
printf("3 returned char: %c\n", ch );

printf("table size: %lu, line size: %lu,  element size: %lu\n",
       sizeof(tab),
       sizeof(tab[0]),
       sizeof(tab[0][0])
       );

printf("number of elements per lines: %lu\n",
       sizeof(tab[0])/sizeof(tab[0][0]));


printf("number of lines: %lu\n",
       sizeof(tab)/sizeof(tab[0]));

return 0;
}

Upvotes: 12

Paul Ogilvie
Paul Ogilvie

Reputation: 25286

All that it does (in C) is allow you to write indexing code in the called funcion without having to do the address calculation yourself, for example:

double d= xu_col[i*row_size + j]; //get element [i,j]

versus

double d= xu_col[i][j];

Upvotes: 5

MSalters
MSalters

Reputation: 179819

The reason it works in C, but not in C++ is simply because it's C code and not C++. The two languages share a history, not a grammar.

The C++ method to pass variable-sized arrays is std::vector, probably by reference if you intend to modify the vector in the function, or by const reference if you don't.

Upvotes: 38

Stephen Docy
Stephen Docy

Reputation: 4788

In C, it is possible to use function parameters to define the size of a variable length array parameter as long as the size comes before the array in the parameter list. This is not supported in C++.

Upvotes: 61

Related Questions