Home > front end >  What are the use cases of class member functions marked &&?
What are the use cases of class member functions marked &&?

Time:01-10

I don't know which C standard presented this feature, but I cannot think of any use cases of this.

A member functions with && modifier will only be considered by overload resolution if the object is rvalue

struct Foo 
{
    auto func() && {...}
};

auto a = Foo{};
a.func();            // Does not compile, 'a' is not an rvalue
std::move(a).func(); // Compiles
Foo{}.func();        // Compiles

Can someone please explain the use case for this?
Why we ever want some routine to be performed only for rvalues?

CodePudding user response:

Ref-qualification was added in . In general, propagation of the value-category is incredibly useful for generic programming!

Semantically, ref-qualifications on functions help to convey the intent; acting on lvalue references or rvalues -- which is analogous to const or volatile qualifications of functions. These also parallel the behavior of struct members, which propagate the qualifiers and categories.

A great example of this in practice is std::optional, which provides the std::optional::value() function, which propagates the value-category to the reference on extraction:

auto x = std::move(opt).value(); // retrieves a T&&

This is analogous to member-access with structs, where the value-category is propagated on access:

struct Data {
    std::string value;
};

auto data = Data{};

auto string = std::move(data).value; // expression yields a std::string&&

In terms of generic composition, this massively simplifies cases where the input may be an lvalue or an rvalue. For example, consider the case of using forwarding references:

// Gets the internal value from 'optional'
template <typename Optional>
auto call(Optional&& opt) {
    // will be lvalue or rvalue depending on what 'opt' resolves as
    return std::forward<Optional>(opt).value(); 
}

Without ref-qualification, the only way to accomplish the above code would be to create two static branches -- either with if constexpr or tag-dispatch, or some other means. Something like:

template <typename Optional>
auto call(Optional&& opt) {
    if constexpr (std::is_lvalue_reference_v<Optional>) {
        return opt.value(); 
    } else {
        return std::move(opt.value()); 
    }
}

On a technical level, rvalue-qualifications on functions provides the opportunity to optimize code with move-constructions and avoid copies in a semantically clear way.

Much like when you see a std::move(x) on a value, you are to expect that x is expiring; it's not unreasonable to expect that std::move(x).get_something() will cause x to do the same.

If you combine && overloads with const & overloads, then you can represent both immutable copying, and mutating movements in an API. Take, for example, the humble "Builder" pattern. Often, Builder pattern objects hold onto pieces of data that will be fed into the object on construction. This necessitates copies, whether shallow or deep, during construction. For large objects, this can be quite costly:

class Builder {
private:
  
    // Will be copied during construction
    expensive_data m_expensive_state;
    ...

public:

    auto add_expensive_data(...) -> Builder&;
    auto add_other_data(...) -> Builder&;
    ...

    auto build() && -> ExpensiveObject {
        // Move the expensive-state, which is cheaper.
        return ExpensiveObject{std::move(m_expensive_state), ...}
    }
    auto build() const & -> ExpensiveObject
        // Copies the expensive-state, whcih is costly
        return ExpensiveObject{m_expensive_state, ...}
    }
    ...
};

Without rvalue-qualifications, you are forced to make a choice on the implementation:

  1. Do destructive actions like moves in a non-const function, and just document the safety (and hope the API isn't called wrong), or
  2. Just copy everything, to be safe

With rvalue-qualifications, it becomes an optional feature of the caller, and it is clear from the authored code what the intent is -- without requiring documentation:

// Uses the data from 'builder'. May be costly and involves copies
auto inefficient = builder.build();

// Consumes the data from 'builder', but makes 'efficient's construction
// more efficient.
auto efficient = std::move(builder).build();

As an added benefit, static-analysis can often detect use-after-move cases, and so an accidental use of builder after the std::move can be better caught than simple documentation could.

CodePudding user response:

What are the use cases of class member functions marked &&?

A use case are "getter" style functions. They traditionally return references to sub objects or owned objects whose lifetime is tied to the super object.

That is however sometimes problematic, since calling the getter on an rvalue will result in a reference to an object that will be destroyed at the end of the expression. That is unsafe:

struct demo {
    int& get_mem() { return mem; }
private:
    int mem;
};

demo get_demo();

int& ref = get_demo().get_mem();
std::cout << ref; // bug

An lvalue ref qualifier prevents such bugs:

int& get_mem() & { return mem; }
int& ref = get_demo().get_mem(); // safely ill-formed

But it also prevents working previously correct code such as:

std::cout << get_demo().get_mem(); // used to be OK; now ill-formed

A solution is to provide two overloads, each with different qualifier:

int& get_mem() &  { return mem; }
int  get_mem() && { return mem; }

It is commonly used with types that wrap other types, with a full set of overloads that simulate all constess and value categories:

const int&  get_mem() const& ;
      int&  get_mem()      & ;
const int&& get_mem() const&&;
      int&& get_mem()      &&;
  •  Tags:  
  • Related