Reason for using std::forward before indexing operator in "Effective Modern C++" example

Viewed 136

Quoting from item 3 ("Understand decltype") of "Effective Modern C++" :

However, we need to update the template’s implementation to bring it into accord with Item 25’s admonition to apply std::forward to universal references:

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i)
{
    authenticateUser();
    return std::forward<Container>(c)[i];
}

Why are we forwarding 'c' in the last statement of 'authAndAccess'? I understand the need for forwarding when we're passing the param to another function from within the original function (to be able to correctly call the overloaded version which takes an rvalue reference or if there's an overloaded version which takes param by value then we can move instead of copy), but what benefit does std::forward give us while calling indexing-operator in the above example?

The example below also verifies that using std::forward does not allow us to return an rvalue reference from authAndAccess, for instance if we wanted to be able to move (instead of copy) using the return value.

#include <bits/stdc++.h>

void f1(int & param1) {
    std::cout << "f1 called for int &\n";
}

void f1(int && param1) {
    std::cout << "f1 called for int &&\n";
}

template<typename T>
void f2(T && param) {
    f1(param[0]);  // calls f1(int & param1)
    f1(std::forward<T>(param)[0]); // also calls f1(int & param1) as [] returns an lvalue reference
}

int main()
{
    std::vector<int> vec1{1, 5, 9};

    f2(std::move(vec1));

    return 0;
}
2 Answers

It depends on how the Container is implemented. If it has two reference-qualified operator[] for lvalue and rvalue like

T& operator[] (std::size_t) &; // used when called on lvalue
T operator[] (std::size_t) &&; // used when called on rvalue

Then

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i)
{
    authenticateUser();
    return std::forward<Container>(c)[i]; // call the 1st overload when lvalue passed; return type is T&
                                          // call the 2nd overload when rvalue passed; return type is T
}

It might cause trouble without forwarding reference.

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i)
{
    authenticateUser();
    return c[i]; // always call the 1st overload; return type is T&
}

Then

const T& rt = authAndAccess(Container{1, 5, 9}, 0);
// dangerous; rt is dangling

BTW this doesn't work for std::vector because it doesn't have reference-qualified operator[] overloads.

You std::forward when you want your code to respect value categories. The most simplistic aspect of that is reference parameters to functions, and indeed classes with overloaded operators that can depend on value categories. But it goes beyond that, since value categories are ingrained into how expressions work in general, even basic ones.

Consider this toy example:

#include <iostream>

template<typename Container, typename Index>
decltype(auto) access(Container&& c, Index i)
{
    return std::forward<Container>(c)[i];
}

struct A {
    A() = default;
    A(A const&) { std::cout << "Copy\n"; }
    A(A &&)     { std::cout << "Move\n"; }
};

int main()
{
    using T = A[1];
    [[maybe_unused]] auto _1 = access(T{}, 0);
    {
        T t;
        [[maybe_unused]] auto _2 = access(t, 0);
    }
}

It will print:

Move
Copy

Why? Because we passed an rvalue raw array into the function. As such, the indexing expression (after forwarding) respects the semantics of the build in []. If you index an rvalue array, you obtain an element that is also an rvalue in the taxonomy of value categories. So we move from it when initializing _1.

_2 on the other hand, is initialized from an lvalue, again because we passed an lvalue array into the function. If you want to let expressions inside a function do their things depending on the value category of the operands, your code can std::forward to accomplish that.

It doesn't mean this makes sense for every expression under the sun, but it's something that is useful to be aware of.

Related