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.