September 30th, 2024

Pulling a single item from a C++ parameter pack by its index, remarks

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::move 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.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

3 comments

  • Dmitry 1 week ago · Edited

    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.

  • Vittorio Romeo

    I was kind of surprised that we cannot write:

    auto&& arg = (std::forward(args)...)[index];

    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.

  • BCS 2 weeks ago

    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.