Search code examples
c++ref-qualifier

When are ref-qualified member functions necessary


As I understand it, ref-qualified member-functions are used to distinguish between operating on an implicit this as an lvalue versus an rvalue. If we define a member function without the ref-qualification, then both lvalues and rvalues call the same member function. For example

class Obj {
public:
  Obj(int x) : x_{x} {}
  int& getVal() {return x_;}
};

here, doing something like Obj o{2}; o.getVal(); and Obj{2}.getVal() call the same getVal() function.

However, I'm not sure I understand the use case for this. I can't seem to see when we might want to treat an lvalue different from an rvalue. I was thinking perhaps rvalues might be treated as temporary variables, so maybe we don't want to return a reference to a member variable, whereas it might be ok for lvalues since they're actually stored in a memory address and can be managed better. For example,

class Obj {
public:
  Obj(int x) : x_{x} {}
  int& getVal() & {return x_;}
  int getVal() && {return x_;}
};

I believe would return a reference to the member x_ for lvalues, and would return a copy of it for rvalues of Obj since Obj might be deleted after calling the function and we want to keep a copy of that returned member. However, I'd imagine then we'd also want to have an rvalue version of it in case we were to work with an rvalue reference in another function and we want to modify the member somehow.

Is my interpretation of ref-qualified member-functions correct so far?


Solution

  • If one was consistent, then non-const non-static member functions would almost always be &-qualified. Otherwise there is inconsistent behavior between member functions and free functions.

    Suppose for example that you have

    struct A {
        int i;
        void inc() { i++; }
        void inc2() & { i++; }
    };
    
    void inc(A& a) { a.i++; }
    
    int main() {
        inc(A{});   // error
        A{}.inc();  // no error
        A{}.inc2(); // error
    }
    

    With this you can not do inc(A{}), but you can do A{}.inc(). And that you can't do inc(A{}) is an intentional design decision of the language, because you are giving a temporary to a function that expects to modify its argument, which presumably ought to be a side effect relevant to the caller, but when passing a temporary that temporary is immediately destroyed again without the caller being able to inspect its modification. That you are however allowed to do the exact same thing if you use a member function instead is kind of inconsistent.

    Of course you never strictly need the ref-qualifiers, but there are some use cases.

    They can be used to disallow what I mentioned above to avoid user mistakes where a temporary would be provided to the object parameter. Or sometimes it may be necessary to require a member function to be callable only on a rvalue, in which case the function must be &&-qualified, e.g. because the function will extract resources from the object and leave it in an unspecified state.

    Sometimes a member function may be able to steal resources of the class if it is called on a rvalue and would otherwise be const qualified, just like you would have an const& and a && overload with free functions. See for example std::string::substr, which should be allowed to steal memory resources when called on a non-const rvalue, but should still be able to operate on a const lvalue.

    There are also instances where you want the member function to forward the value category to some other function or as return value, in which case you need both &- and &&-overload (+ potentially const/volatile variations) to distinguish the value category given to the member function. See std::optional::value for an example of this.

    In C++23 there are explicit object parameters, so that one can use normal forwarding references for the last use case instead. With explicit object parameters it is also possible to simply write a non-static member function that looks and behaves exactly like the free function:

    struct A {
        int i;
        void inc(this A& a) { a.i++; } // essentially equivalent to void inc() & { i++; }
    };
    
    int main() {
        A a{};
        a.inc();    // no error
        A{}.inc();  // error
    }