how to print an n-dimensional array in c++

Viewed 373

I would like to write a function that can print different arrays. For instance:

#include<iostream>
using namespace std;
int main(){
    int a[10];
    int b[3][2];
    for(int i = 0; i < 10; i++){
        a[i] = i;
    }
    for(int i = 0; i < 3; i++){
        for(int j = 0; j < 2; j++){
            b[i][j] = i * 2 + j;
        }
    }
    print_arr(a, /*some input*/);
// output: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
    print_arr(b, /*some input*/);
// output: 
// 0, 1
// 2, 3
// 4, 5
   return 0;
}

can somebody help me or say that this is impossible. maybe this question is already answered. In that case please can you share the link of that question

2 Answers

You could create a function template that unwraps the array recursively.

Example:

#include <iostream>
#include <type_traits>

// only enable the function for arrays
template<class T, std::enable_if_t<std::is_array_v<T>, int> = 0>
void print_arr(const T& x) {
    for(auto& in : x) {
        if constexpr (std::rank_v<T> > 1) // more dimensions to go
            print_arr(in);                // call to unwrap next dimension
        else
            std::cout << in << ' ';       // last dimension, print the value
            
    }
    std::cout << '\n';
}

Demo

Since the name print_arr suggests that you will not need SFINAE you could also replace the enable_if_t part of the template with a static_assert inside the function:

template<class T>
void print_arr(const T& x) {
    static_assert(std::is_array_v<T>);
    // ...

Instead of streaming directly to std::cout, you could add a std::ostream& parameter to make it stream to any stream

template<class T>
void print_arr(std::ostream& os, const T& x) {
    static_assert(std::is_array_v<T>);
    for(auto& in : x) {
        if constexpr (std::rank_v<T> > 1)
            print_arr(os, in);
        else
            os << in << ' ';
            
    }
    os << '\n';
}

// ...

    print_arr(std::cout, a);
    print_arr(std::cout, b);

Or you could make it return a std::string of the complete output which lets you do what you want with it afterwards.

Example:

#include <sstream>

template<class T>
std::string print_arr(const T& x) {
    static_assert(std::is_array_v<T>);
    std::ostringstream os;

    for(auto& in : x) {
        if constexpr (std::rank_v<T> > 1)
            os << print_arr(in);
        else
            os << in << ' ';
            
    }
    os << '\n';
    return os.str();
}

// ...

    std::cout << print_arr(a);
    std::cout << print_arr(b);

Ted Lyngmo's answer is definitely the most concise, and probably the closest to achieving what you asked for, but there's still a few things worth pointing out including an alternate solution that might make sense in some circumstances.

First of all, it's important for anyone reading this to understand that C and C++ types don't include any kind of runtime type information by default. Yes it's true, RTTI exists for record types, but a built-in C array like int arr[2][3] is not much more than a bit of sugar to say "give me a region of memory large enough to store 6 ints". This is in contrast for example to a language like python, where a list object will include metadata about the size and contents. It's this metadata that allows you to write very flexible functions than can query the object itself about its structure, and of course it comes at a cost.

When we introduce a templated solution like the one above, we haven't added runtime type information, we've asked the compiler to generate code for every possible permutation of array type that we might need. This is fine if you only have a couple of them, but if the function gets used a lot, with many different array types, things grow really fast. This could actually become a problem in some environments. Consider for example the following main function:

int main(int argc, char **argv)
{
    int arr1[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
    print_arr(arr1);
    int arr2[3] = {13, 14, 15};
    print_arr(arr2);
    int arr3[3][2][2] = {{{10, 20}, {30, 40}}, {{50, 60}, {70, 80}}, {{90, 100}, {110, 120}}};
    print_arr(arr3);
    return 0;
}

And now look at the AST that gets generated:

jon@prompt$ clang++ -fsyntax-only -Xclang -ast-dump test.cpp -std=c++17 | grep FunctionDecl | grep print_arr
| |-FunctionDecl 0x10921fd88 <line:6:1, line:15:1> line:6:6 print_arr 'void (const T &)'
| |-FunctionDecl 0x109224c18 <line:6:1, line:15:1> line:6:6 used print_arr 'void (int const &[3][4])'
| |-FunctionDecl 0x109225ab8 <line:6:1, line:15:1> line:6:6 used print_arr 'void (int const &[3])'
| |-FunctionDecl 0x1092270a8 <line:6:1, line:15:1> line:6:6 used print_arr 'void (int const &[3][2][2])'
| |-FunctionDecl 0x10933db08 <line:6:1, line:15:1> line:6:6 used print_arr 'void (int const &[4])'
| |-FunctionDecl 0x1093539a8 <line:6:1, line:15:1> line:6:6 used print_arr 'void (int const &[2][2])'
| `-FunctionDecl 0x1093552d8 <line:6:1, line:15:1> line:6:6 used print_arr 'void (int const &[2])'

Here you see the compiler has emitted a different version of the print_arr function for every permutation of array size we've used. This isn't exactly what you could reasonably call "one function", it's actually many functions that have each been automatically generated by the compiler. It's a tiny amount of text to write in a cpp file, but the actual amount of code that gets generated is significant.

An alternate way to solve this problem is to just include type information in the print_arr function. Here's an example of an implementation that is WYSIWYG, and will let you do the same thing though you need to provide the type metadata manually:

const int *print_arr(const int *arr, const int rank, const int *lengths)
{
    const char *p_sep = "";
    printf("{");
    if (rank > 1) {
        for (int i = 0; i < lengths[0]; i++) {
            printf("%s", p_sep);
            arr = print_arr(arr, rank - 1, &lengths[1]);
            p_sep = ", ";
        }
    } else {
        for (int i = 0; i < lengths[0]; i++) {
            printf("%s%d", p_sep, *arr++);
            p_sep = ", ";
        }
    }
    printf("}");
    return arr;
}

And here's how you would use it with the same example arrays:

int main(int argc, char **argv)
{
    int arr1[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
    print_arr(&arr1[0][0], 2, (int[2]){3, 4});
    printf("\n");
    int arr2[3] = {13, 14, 15};
    print_arr(&arr2[0], 1, (int[1]){3});
    printf("\n");
    int arr3[3][2][2] = {{{10, 20}, {30, 40}}, {{50, 60}, {70, 80}}, {{90, 100}, {110, 120}}};
    print_arr(&arr3[0][0][0], 3, (int[3]){3, 2, 2});
    printf("\n");

    return 0;
}

Output:

jon@promptC$ ./test
{{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}}
{13, 14, 15}
{{{10, 20}, {30, 40}}, {{50, 60}, {70, 80}}, {{90, 100}, {110, 120}}}

This approach has a few advantages, the most important being that the compiler emits pretty much exactly what's written. It also forces you to be very explicit in the code, which is probably good in this case.

As always with C and C++ you need to know what's most appropriate for the situation you find yourself in.

Related