In my earlier discussion of pulling a single item from a C++ parameter pack by its index, I noted that with the Pack Indexing proposal, you would be able to write this to pull an item from a parameter pack while preserving its reference category.
template<int index, typename...Args> void example(Args&&... args) { auto&& arg = (Args...[index]&&)args...[index]; using Arg = Args...[index]&&; }
Why did I need to apply the (Args...[index]&&)
cast? Why can’t I just write this:
auto&& arg = args...[index];
or possibly
decltype(auto) arg = args...[index];
Well, when you write the name of a variable, the result is an lvalue reference, even if the variable is an rvalue reference. Watch:
struct S { S(S&); // construct from lvalue S(S&&); // construct from rvalue }; void whathappens(S&& s) { S t = s; // which will it use? }
Try it out in your favorite compiler. This code constructs from an lvalue reference. Even though s
is an rvalue reference, when you say its name, you get an lvalue reference, so that’s the construtor that gets selected.
You sort of knew this already. For example, you can’t take the address of an rvalue reference, but you can write this:
void whathappens(int&& v) { int* p = &v; // legal! }
And you’ve been writing std::
to say “It’s okay to move from this object.”
void whathappens(S&& s) { S t = std::move(s); // force rvalue }
I mean, that’s why you’ve been writing std::move
and std::forward
all these years. If writing s
already produced an rvalue reference, then there would be no need to std::move
or std::forward
it.
“Okay, I get it. Writing the name of a variable that represents an rvalue reference produces an lvalue. So what?”
Since writing the name of the variable produces an lvalue reference, decltype(auto)
sees that the right hand side is an lvalue reference, so it deduces an lvalue reference.
Now, you could say “Well, sure, but let’s make a special case for indexed elements from a parameter pack, so that saying their name produces an rvalue reference if the corresponding parameter is an rvalue.” But that creates another weird special case in C++, and C++ is hard enough to language-lawyer without adding even more weird special cases.¹
Bonus chatter: Instead of
auto&& arg = (Args...[index]&&)args...[index];
I could also have written
auto&& arg = std::forward<Args...[index]>(args...[index]);
which is wordier but probably clearer. I was sort of assuming people understood this common shortcut.
¹ I generally believe in the principle that it is better to have a set of simple rules that are easy to understand and explain, even if it means that some scenarios are awkward or suboptimal, as opposed to a set of rules that cover all scenarios but which are so complex that nobody can understand them, much less explain them.
That’s why when faced with choice between C++ and assembly language with no special requirements I’d choose the latter as it’s much cleaner. And (you know what?) it deals with more cases. Except the AT&T syntax, of course, which is obviously intended for first-year-student-written compiler (assembler) consumption, not for normal human beings or compilers/assemblers.
I was kind of surprised that we cannot write:
I briefly looked at the paper and it seems the grammar specifically takes a “pack-id”. 🙁
I would suggest mentioning this in the article as I expect more people would try that approach.
Regarding the footnote, if you could make a set of rules that actually “cover[s] all scenarios” I’d prefer that regardless of complexity, but that will never happen. And if you can’t have a system that automatically deals with everything, it needs to be understandable. Tl;dr; once we restrict to the options that are available in the real world, I agree with the original claim.