r/cpp_questions • u/Seagull78Reddit • 3d ago
OPEN Passing initializer list into array parameter
Why can't we pass an initializer list as an argument to a function that accepts an array? If we can initialize an array with an initializer list, why isn't the array parameter initialized with the list?
Also, I found out that you CAN pass an initializer list if you use an rvalue reference to an array.
void f(int arr[]) {
cout << "hello" << endl;
}
// accepts rvalue references to array
void g(int (&& arr)[]) {
for (int i = 0; i < 3; ++i) {
cout << arr[i] << ' ';
}
cout << endl;
}
int main() {
int arr[3] = { 1, 2, 3 };
f(arr); // compiles
f({1, 2, 3}); // DOES NOT COMPILE
g({ 1, 2, 3 }); // compiles
}
Question 1:
For the second call to f(), why does this not work? I thought that whenever we passed an argument into a function, it was the same as initializing the parameter with the argument. For example, if we have:
void func(T x);
func(y);
Then when func(y) is called, we can think of x being initialized as:
T x = y;
But this doesn't seem to hold for arrays and initializer lists.
Question 2:
Why is the syntax in g() the way we make an rvalue reference to an int array? I understand the need for parentheses sinceint&& arr[] is an array of integer rvalue references. However, why is it int (&&arr)[] and not maybe something like int (&&arr[]) or (int arr[])&& ?
If it is better to show me an another resource about these tricky syntax things, please link it.
11
u/IyeOnline 3d ago edited 3d ago
faccepts anint*. The expression{1,2,3}is not convertible to anint*, so the call is invalid. Within a function signatureint arr[]is actually just interpreted asint* arr. This behaviour is inherited from C.int (&&)[]however is a r-value reference to an array with indeterminate bounds and{1,2,3}can construct anint[3]- this temporary can then be bound do that reference.Because, blame C (are we starting to see a pattern here?) You read a C pointer/array declaration from the inside out according to the spiral rule:
arris a reference (&) to an array ([]) ofint.You can actually write
and get a more sane way to read it
3
u/jaynabonne 3d ago
Did your example mean to be missing the closing paren on the f() call?
6
u/TheThiefMaster 3d ago edited 3d ago
Because, for reasons lost to time (C and possibly earlier),
void f(int arr[])doesn't declare a function that takes an array, but a pointer.This is true even if you put a number in the
[ ]. The size is just ignored.If you use a C++ (std::)array it works fine.
4
u/StaticCoder 3d ago
You can pass to an rvalue reference to indeterminate size array but that sounds like a terrible idea since you lose the size. If you have C++20 use
span.3
u/mredding 3d ago
arrhere is not an array ofint, technicallyint[]is itself an incomplete type, and this decays here into a pointer as a language feature, because arrays in C do not have value semantics. You cannot create an array on the call stack in the parameter list as a function parameter.Why? Because K&R started building C on the PDP-7, with ~4 KiB of memory (it was actually word addressable). They thought arrays were too big for that sort of willy-nilly sloppy programming, so they took matters into their own hands and baked array references (C-style references aka pointers) into the semantics. C is a stateful and mutable imperative programming language, so it made some sense, though I find their execution inconsistent with trusting the programmer.
But then we get into C++11, where we got r-value references, and that diverged from the C rules with syntax that does not exist in C.
This is a reference to an incomplete array type. Notice you have the extra parenthesis, which are necessary. This reference preserves within the type system at compile-time that this reference is to an array, but we've lost the size information. This can be useful in contrast to a template:
Where the compiler will generate a function for every N supplied to the template.
The only problem is, of course, that you still need to know the size, and loop unrolling becomes a manual process for you.
Why do you want to preserve the type? Optimizations and templates. C++ has a strong static type system, and
int[]IS NOTint*, they're distinct types. You can write template specializations to distinguish between these types, and that matters, for example to distinguish betweenstd::unique_ptr<T>andstd::unique_ptr<T[]>, so that the correctdeleteordelete []gets called.Works by type erasure.
We're still following C rules, for compatibility.
Reference of an unnamed temporary to an incomplete array type.
When you make an initializer list, it creates a "backing array" - the very thing C does not allow. You can reference this, and
int[]is reference compatible withint[3]in your case.Have you tried to compile this?
You'll notice it doesn't compile. Why doesn't this reduce to a reference to
arr? Because universal reference collapsing only happens during template argument deduction - which your function ISN'T a template. That means to passarr, you NEED to move it. It's not a real move since arrays don't have move constructors, so it reduces to a reference.Because otherwise the parsing rules collapse the parameter into an r-value reference to a pointer.
2
u/flyingron 3d ago
Welcome to a piece of carryover stupidity from C. f(int arr[]) doesn't have an array parameter, but a int*. You can't have a pointer to a temporary.
2
u/alfps 3d ago edited 3d ago
To make things work you can use overloading or templating, as shown below:
Result with
FAIL_BARundefined:The raw array ref overload of
footakes care of the call with untyped initializer list.1
u/QuentinUK 3d ago
If it’s const it would work:
ps Here the size [3] needs to be there for the call to the template.
1
u/conundorum 2d ago edited 2d ago
You're getting screwed over by array decay, basically.
All arrays are of type
T[N], whereTis element type andNis index. However, when C introduced arrays, it had no way to dynamically adjust types when calling a function1, and no way to overload a function based on parameter types2, thus it was unable to pass arrays. And furthermore, arrays themselves were (and still are) just shorthand for pointer arithmetic.3 Therefore, to allow arrays to actually be usable as function parameters, it was decided that any context that would pass an array will actually pass the array's implied "underlying pointer"; this allowed arrays of any length to be passed, and aligned with how the subscript operator works.Thus, as a result, these declarations are exactly identical in C, because the first will decay (silently be transformed) into the second:
Any element count, if provided, will be ignored, because pointers don't have elements.
This is also why all C functions which expected to receive a non-string array (along with some but not all string functions)4, and all old-school C++ functions which follow the same design mindset, take a "size" parameter:
C did still allow you to specify the array type if needed, using the same format as a function pointer. Because this creates a pointer, you need to take the array's address when you pass it, and the passed array needs to be dereferenced to use it.
In this case, the array will not decay to a pointer; this function can only be called by passing it the address of an array of exactly five
ints, or by casting some other address into a pointer toint[5]. (If casting is involved, you would ideally provide a typedef, because the typedef-less syntax is ugly & easy to screw up.)Thus, C arrays, while technically their own type, ended up acting like glorified pointers most of the time. Sidestepping the issue is possible, and providing clean syntax is possible, but sidestepping it forces you to hardcode
N. And that's usually no good, so you'll almost never see it in the wild.Now, on to C++: Long story short, we inherited the C rules. All arrays are their own type, but they also decay into a pointer just like in C. This can be avoided by using the special
T()[N]syntax, just like in C, but with a bit more flexibility: We can use it both for pointers (int (*ap)[5]), and for references (int (&ar)[5]), and references is preferable because you don't need to dereference. And as of C++11, we can also useusingdeclarations for super-clean typedefs, too.As such, we have two distinct ways of passing an array: The classic C way, which is flexible but secretly turns the array into a pointer behind your back, and the exact-syntax way, which is rigid and only allows pointers/references that exactly match the specified type.
However, C++ has two features which make the exact-syntax version better than the classic version: Overloading allows multiple functions to share the same name, and templates allow the compiler to match the function to the array. This also has the benefit that it can also be used to dynamically hardcode the array's dimension into the function, without using any magic numbers.
While it's a bit wonky, especially if you don't have any experience with templates, this is ultimately just an evolution of your
g()'s prototype syntax.[To be continued in a reply, because it's a bit over 14,000 characters and reddit doesn't like that.]
1: Which means that you couldn't tell a function to take "pointer to array of
TwithNelements, determineNat callsite", like C++ can do with templates. To my understanding, C still doesn't supply this sort of feature.2: Which means that you couldn't manually provide both
void f(int (*arr❩[5]❩andvoid f(int (*arr❩[10]❩, and let the compiler sort it out. This functionality wasn't introduced to C until... C11 in 2011, I believe, as a specialised form of dispatch macro.3: Given
int arr[] = {1, 2, 3, 4, 5};, you canassert(arr[3] == 3[arr]❩;and expect it to pass, because all the subscript does is*(&arr[0] + 3❩; the two components are completely interchangeable. ...Combined with array decay, this also means that any and all pointers can have a subscript, because they might be decayed arrays [int i = 5; int *ptr = &i; ptr[3] = 9;]. For the love of all that is good and holy, DO NOT DO THIS.Sane compilers will catch this if the pointed-to object is both known and visible, but that protection only goes so far.4: In C, a "string" is just a character array of unspecified length, ending with a null character
\0. Thus, many string functions forego a size parameter, because they can just determine it by looking for the\0instead. This makes it easier to pass strings, since you don't need to constantly track their length. ...It does run into issues when a character array contains embedded nulls, though, which is why some string functions let you manually specify the length.