Is it possible to prevent array-to-pointer decay in arguments expanded from a parameter pack?
For example:
#include <iostream>
void foo() {
std::cout << "empty\n";
}
template <typename T, typename... Rest>
void foo(T &&t, Rest... rest) {
std::cout << "T, ...\n";
foo(rest...);
}
template <typename... Rest>
void foo(char *p, Rest... rest) {
std::cout << "char*, ...\n";
foo(rest...);
}
template <int N, typename... Rest>
void foo(char (&first)[N], Rest... rest) {
std::cout << "char[], ...\n";
foo(rest...);
}
int main() {
char a[2], b[2], c[2];
foo(a, b, c);
}
...outputs:
char[], ...
char*, ...
char*, ...
empty
As you can see, the first call goes to the array-based overload, but subsequent calls go to the pointer-based overload. Is there any way to get all of the calls to go to the array-based overload?
Related: Problems specializing variable template function
To prevent array decay Array decay is prevented by passing the size of array as a parameter and do not use sizeof() on parameters of array. Pass the array into the function by reference. It prevents the conversion of array into pointer and it prevents array decay.
std::decay Applies lvalue-to-rvalue, array-to-pointer, and function-to-pointer implicit conversions to the type T , removes cv-qualifiers, and defines the resulting type as the member typedef type . Formally: If T names the type "array of U " or "reference to array of U ", the member typedef type is U*.
You want to pass the parameter pack by rvalue reference:
void foo(char (&first)[N], Rest&&... rest)
^^
So the code looks like this overall:
#include <iostream>
void foo() {
std::cout << "empty\n";
}
template <typename T, typename... Rest>
void foo(T &&t, Rest... rest) {
std::cout << "T, ...\n";
foo(rest...);
}
template <typename... Rest>
void foo(char *p, Rest... rest) {
std::cout << "char*, ...\n";
foo(rest...);
}
template <int N, typename... Rest>
void foo(char (&first)[N], Rest&&... rest) {
std::cout << "char[], ...\n";
foo(rest...);
}
int main() {
char a[2], b[2], c[2];
foo(a, b, c);
}
Giving the result:
char[], ...
char[], ...
char[], ...
empty
I haven't changed the other overloads to do the same, but you'd normally want them to use an rvalue reference as well (if they were actually being used).
Edit: As to why you'd want to do this/why it works: an rvalue reference can bind to either an rvalue or an lvalue. The crucial point we care about here is that when it binds to an lvalue, it remains an lvalue. In the case of an array, it retains its identity as an array, so what's received is an array.
When/if we pass an array by value, it undergoes the normal "decay" to a pointer, just like with a normal function.
For this specific case, we could also use a normal lvalue reference -- but if we did, that would not work for any type that wasn't an lvalue. For example, if we tried to call foo(1,2,3);
, we'd get an error because an lvalue reference can't bind to 1
, 2
or 3
. To deal with that we could pass a const
lvalue reference, but then we wouldn't be binding the reference directly to the rvalue -- we'd be creating a temporary containing a copy of the rvalue that was passed, and then binding the lvalue reference to that temporary copy instead. For the specific case of an int, that probably wouldn't be a major problem, but with something that was more expensive to copy (or if we wanted access to the original, not a copy) that could be a problem.
@JerryCoffin's answer hit the spot already, but I wanted add a small remark. You can separate the list processing code from the item one like this:
void foo_list() {
std::cout << "empty\n";
}
template <typename T, typename... Rest>
void foo_list(T &&t, Rest&&... rest) {
foo(t);
foo_list(rest...);
}
template <int N>
void foo(char (&t)[N]){
// ...
}
void foo(char *){
// ...
}
// etc...
(Maybe there's already an idiom for that?).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With