Search code examples
c++perfect-forwardingc++26

Universal references: Why deducing this does not have std::forward?


I watched a video of Scott Meyers about universal references and he told that anytime when you use a universal reference, you must forward it like that:

template<typename T>
auto func(T &&a) { return ++std::forward<T>(a); }

Then with deducing this we have:

auto member_func(this auto &&self) { return self.member_variable; }

Where is the std::forward<???>(self).member_variable here? What I missed?


Solution

  • Generally the forwarding reference in the explicit object parameter should be forwarded just as any other forwarding reference:

    auto member_func(this auto &&self) {
        return std::forward<decltype(self)>(self).member_variable;
    }
    

    In general there is nothing different here from other forwarding references. You didn't state where you got the code from, so I can't tell why the author decided to not forward or whether it is a problem in this specific instance.

    However, because this is the explicit object parameter of a member function, you typically can assume what type self will have, contrary to most forwarding references in general function templates. It will be the type of the class in which it is written or a derived class type.

    In that case it is reasonable to assume that member_variable will refer to the member member_variable declared in the same class. And you know what type that member has.

    Now suppose it looks like this:

    struct A {
        int member_variable;
        auto member_func(this auto &&self) { return self.member_variable; }
    };
    

    Then the type of self.member_variable can reasonably be assumed to be int and for int it makes absolutely no difference whether it is returned by-value (auto) from a lvalue or rvalue expression. The std::forward call would be known to be without effect.

    Problems with this:

    1. The member function is not constrained to the class type in which it appears itself. If the class type is not marked final or the member function declared private, then it could be possible that it will be called on a derived class type that also defines a member_variable of a different type. That would be found for return self.member_variable; and it could have a type where forwarding could make a difference, e.g. std::string.

      However, it is not obvious to me that the author of the code considered that scenario, or whether they actually want to always return the member_variable of the class itself. In that case it should be self.A::member_variable or something along this way. There are still issues with multiple inheritance and private inheritance in that case. (There is a solution for private inheritance though.)

    2. If the forwarding reference is not forwarded, then there is no point to have the explicit object parameter be this auto&& in this case at all. It could simply be declared as this const auto& or this const A&, which would also avoid the issue mentioned above. In fact it could then simply be a normal non-static member function without explicit object parameter.

    3. Because function pointers to explicit member functions can be formed, it is possible to call the member function indirectly through such a pointer with any other type for self. Similarly, an implicit conversion to any type can be forced by explicitly specifying the template argument for the auto type. However, these are not normal use cases and the function probably doesn't need to consider them.


    Also, in general it is important to remember what using std::forward means. Naively applying std::forward to any appearance of a variable that is a forwarding reference is wrong. The std::forward causes the expression to which it is operand to (potentially) consume the object's state. It can generally only be applied once in the function body and afterwards the object shouldn't be used anymore.

    In case of a member access expression like

    return std::forward<decltype(self)>(self).member_variable;
    

    it is a bit different: Here std::forward causes potential consumption of only the member_variable member. The remaining members are still guaranteed to be in their previous state. So it is possible to apply std::forward once for each member individually.