0

The following code compiles with gcc but not g++. Is it possible to write a function with a matrix argument of arbitrary dimensions in C++?

void print_mat(const int nr, const int nc, const float x[nr][nc]);
#include <stdio.h>

void print_mat(const int nr, const int nc, const float x[nr][nc])
{
    for (int ir=0; ir<nr; ir++) {
        for (int ic=0; ic<nc; ic++) {
            printf(" %f",x[ir][ic]);
        }
        printf("\n");
    }
}
Fortranner
  • 2,525
  • 2
  • 22
  • 25
  • 3
    Not like this. C++ doesn't support [Variable Length Arrays](https://en.wikipedia.org/wiki/Variable-length_array). In C++, an array of dynamically determined size is usually spelled `std::vector` – Igor Tandetnik May 12 '22 at 00:13
  • This would be valid C99 code. C++ went [a different way](https://en.cppreference.com/w/cpp/container/vector). Which is better is a matter of opinion, but C11 made the Variable Length Arrays (VLA) optional and some of those who see VLAs as the weaker option see this change as an admission of a misstep. – user4581301 May 12 '22 at 00:28
  • @John3136 C++ doesn't work like that (and neither does C, for that matter). And if they even did, the row stride would be indecipherable, and as such the pointer arithmetic to the superior dimension wouldn't work. `type arr[][]` isn't valid in *either* language, and never has been. – WhozCraig May 12 '22 at 00:30
  • @WhozCraig Yeah ok. I jumped in too fast :-( Been too long since I've passed around raw multi dimension arrays. – John3136 May 12 '22 at 00:36
  • `g++` will flag this but `clang++` (even with `-std=c++17`) will _not_: `#include void print_mat(const int nr, const int nc, const float (*x)[nc]) { for (int ir = 0; ir < nr; ir++) { for (int ic = 0; ic < nc; ic++) { printf(" %f", x[ir][ic]); } printf("\n"); } }` – Craig Estey May 12 '22 at 00:53
  • @WhozCraig See my above comment with the example. Any ideas as to who is right/wrong (e.g. `g++/clang++`) and why? I'm not sure it's a VLA [in the sense of allocated on the stack] and the caller could pass a malloc'ed pointer – Craig Estey May 12 '22 at 01:16
  • `C` is not `FORTRAN`, `C++` is not `FORTRAN`. There are no multi-dimensional arrays. There are array, array of array, array of array of array and so on, and all but the last dimension can be omitted because that is the way indexes are calculated. Arrays are just base and displacement. and element size. Elements are placed line by line. Anyway you can just pass the so called dimensions and a pointer and do the math – arfneto May 12 '22 at 02:54
  • See my answer here ([How to use multidimensional (ex: 2D) arrays, and pointers to them, as function parameters in C and C++](https://stackoverflow.com/a/67814330/4561887)), approach 3: _"If the 2D array has a VARIABLE number of rows AND a VARIABLE number of columns, do this:"_ `void print_array4(const int *array_2d, size_t num_rows, size_t num_cols)`. I show two distinct techniques within that approach. – Gabriel Staples May 12 '22 at 03:24
  • This question may be a duplicate of [How to pass a multidimensional array to a function in C and C++](https://stackoverflow.com/q/2828648/4561887)? – Gabriel Staples May 12 '22 at 03:24
  • @user4581301, VLA were not a misstep. They will become mandatory again in C23. Only automatic VLAs on stack will stay optional – tstanisl May 12 '22 at 08:01
  • @IgorTandetnik, VLA were added for a convenient handling of runtime-sized multidimensional arrays. C++ lacks support for such an object, a rare case when C is actually better. The `vector` of `vector`s is a workaround but it adds large overhead especially for small internal dimension. – tstanisl May 12 '22 at 08:21
  • @CraigEstey, it is VLA, contrary to popular belief one can have VLAs on heap. Just use a pointer to VLA. Try: `int (*arr)[n] = malloc(sizeof *arr);` – tstanisl May 12 '22 at 08:30
  • @arfneto No, only the one leftmost dimension can be omitted. All the others have to be specified as compile-time constants. The arithmetic the compiler has to do to access `a[x][y][z]` in the array `int[L][M][N]` is `x*M*N + y*N + z` - observe how this expression uses all dimensions except the leftmost. – Igor Tandetnik May 12 '22 at 12:45

2 Answers2

0

As noted in comments, C++ does not support variable-length arrays (VLAs). C did from the 1999 standard, but that became optional in C11. In combination, those factors are relevant to why gcc (depending on version) accepts your code, but g++ does not.

In C (and C++ if writing in a C style <blech!>), an alternative is to pass a single-dimensional array (with contiguous elements) to a function that accepts a pointer and use an indexing scheme to access elements. For example, assuming row-major ordering;

void print_mat(const int nr, const int nc, const float *x)
{
    for (int ir=0; ir<nr; ir++)
    {
        int row_start = ir * nc;
        for (int ic=0; ic<nc; ic++)
        {
            printf(" %f",x[row_start + ic]);
        }
        printf("\n");
    }
}

In C++, one can use - depending on which (if any) dimensions are known at compile time;

  • std::array<std::array<float, nc>, nr> (if array dimensions nc and nr are both fixed at compile time);
  • std::vector<std::vector<float> > (if neither dimension is known until run time). Bear in mind that individual std::vector<float>s in a std::vector<std::vector<float> > CAN have different dimensions. Your caller will need to ensure dimensions are the same for all contained std::vector<float>s and/or your function will need to check sizes.

If nc is fixed at compile time but nr is not, you can use std::vector<std::array<float, nc> >. If nr is fixed at compile time, but nc is not, you can use std::array<std::vector<float>, nr>.

If you must pass the entire vector/array, usually better to pass it by reference than by value. For example;

void print_mat(const std::array<std::array<float, nc>, nr> &)
{
     // definition
}

or (if you need to pass around some arrays of different dimensions) create a family of such functions

template<int nc, int nr>
void print_mat(const std::array<std::array<float, nc>, nr> &)
{
     // definition
}

Personally, I would not actually pass arrays or vectors around. I'd use iterators, such as;

template<class NestedIterator>
void print_mat(NestedIterator row, NestedIterator end_row)
{
     while (row != end_row)
     {
           auto col = std::begin(*row);     // Assuming C++11 and later
           auto col_end = std::end(*row);
           while (col != col_end)
           {
               std::cout << ' ' << *col;
               ++col;
           }
           std::cout << '\n';   // or std::endl
           ++row;
     }
}

This function assumes begin and end iterators from a container that contains (nested) containers (so passing iterators from a std::vector<float> will be a diagnosable error). It works for any type of element (e.g. is not limited to float in your case), that can be streamed to a std::ostream.

I've assumed row-major ordering in the above. The adjustments for column-major ordering are trivial.

Peter
  • 35,646
  • 4
  • 32
  • 74
  • Pointer to VLAs will be mandatory again in upcoming C standard known as C23. Only infamous automatic VLAs on stack will stay optional. – tstanisl May 12 '22 at 08:10
  • @tstanisl At this time, in early/mid 2022, C23 is a set of proposed/draft updates of the C standard. There is no certainty, as yet, that any proposed changes will actually make the cut, or be ratified. Given the number of features that are proposed and either rejected outright or consideration deferred to a later standard (which happens with any evolving standard, not just C) I don't really consider it appropriate to describe C23 features in a response to a "how do I?" question like this one. – Peter May 12 '22 at 13:41
0

To build on Peter’s answer, you can use the single-dimension variant with proper indexing to do the work. But you can make invoking the function much nicer in C++:

void print_mat(const int nr, const int nc, const float *x)
{
  ...
}

template <std::size_t NumRows, std::size_t NumColumns>
void print_mat(const float (*x)[NumRows][NumColumns])
{
  print_mat((int)NumRows, (int)NumColumns, (const float *)x);
}

Now you can use the function naturally:

float matrix[4][3] = { ... };

print_mat( matrix );

This only works, however, as long as you do not let the array downgrade to a pointer.

Also, there are limit issues with the cast from size_t to int, but it really shouldn’t be possible to make one big enough that it would matter.

EDIT: There are also potential buffering/alignment issues when casting a multidimensional array to a one-dimensional, flat array. But no common, modern compiler + hardware that I am aware of where this is an issue. Just be sure to know your target platform.

Dúthomhas
  • 8,200
  • 2
  • 17
  • 39
  • The `print_mat` function will not work for an array for which the first dimension is run-time defined while the second is fixed. For example `int (*arr)[5] = new int[n][5];`. – tstanisl May 12 '22 at 08:15