What's a Use Case for Overloading Member Functions on Reference Qualifiers

What's a use case for overloading member functions on reference qualifiers?

In a class that provides reference-getters, ref-qualifier overloading can activate move semantics when extracting from an rvalue. E.g.:

class some_class {
huge_heavy_class hhc;
public:
huge_heavy_class& get() & {
return hhc;
}
huge_heavy_class const& get() const& {
return hhc;
}
huge_heavy_class&& get() && {
return std::move(hhc);
}
};

some_class factory();
auto hhc = factory().get();

This does seem like a lot of effort to invest only to have the shorter syntax

auto hhc = factory().get();

have the same effect as

auto hhc = std::move(factory().get());

EDIT: I found the original proposal paper, it provides three motivating examples:

  1. Constraining operator = to lvalues (TemplateRex's answer)
  2. Enabling move for members (basically this answer)
  3. Constraining operator & to lvalues. I suppose this is sensible to ensure that the "pointee" is more likely to be alive when the "pointer" is eventually dereferenced:
struct S {
T operator &() &;
};

int main() {
S foo;
auto p1 = &foo; // Ok
auto p2 = &S(); // Error
}

Can't say I've ever personally used an operator& overload.

Why is overloading on just one ref-qualifier not allowed?

It's not any different to the following situation:

struct S {};

void g(S s);
void g(S& s);

int main()
{
S s;
g(s); // ambiguous
}

Overload resolution has always worked this way; passing by reference is not preferred to passing by value (or vice versa).

(Overload resolution for ref-qualified functions works as if it were a normal function with an implicit first parameter whose argument is *this; lvalue-ref qualified is like a first parameter S &, const & is like S const & etc.)

I guess you are saying that g(s) should call g(S&) instead of being ambiguous.

I don't know the exact rationale, but overload resolution is complicated enough as it is without adding more special cases (especially ones that may silently compile to not what the coder intended).

As you note in your question, the problem can be easily avoided by using the two versions S & and S &&.

Use case for `&` ref-qualifier?

They are useful for both providing safety and optimizations.

For member functions returning a pointer to something they own (either directly or via view types like std::string_view or std::span), disabling the rvalue overloads can prevent errors:

struct foo {
int* take_ptr_bad() { return &x; }
int* take_ptr() & { return &x; }
int x;
};

foo get_foo();

void bar() {
auto ptr = get_foo().take_ptr_bad();
// ptr is dangling
auto ptr = get_foo().take_ptr();
// does not compile
}

The other is to provide some optimizations. For instance, you might overload a getter function to return an rvalue reference if this is an rvalue to prevent unnecessary copies:

struct foo {
const std::string& get_str() const & {
return s;
}

std::string&& get_str() && {
return std::move(s);
}

std::string s;
};

void sink(std::string);

foo get_foo();

void bar() {
sink(get_foo().get_str());
// moves the string only if the r-value overload is provided.
// otherwise a copy has to be made, even though the foo object
// and transitively the string is temporary.
}

These are how I use the feature, and I'm sure there are more use cases.

Is there any real use case for function's reference qualifiers?

There are basically two uses:

  1. To provide an optimized overload, for example to move a member out of a temporary object instead of having to copy it.
  2. Prevent misuse of an API. For example, no one would expect

    int a = 1 += 2;

    to work and this would also cause a compile error. However

    string b = string("foo") += "bar";

    is legal if operator += is declared as

    string & operator += (string const & o);

    as is usually the case. Also this has the nasty side-effect of providing an lvalue-reference to your rvalue. Bad idea. This can easily be prevented by declaring the operator as

    string & operator += (string const & o) &;

Overload resolution with ref-qualifiers

Firstly, the implicit object parameter is treated as a normal parameter as per 13.3.1.4:

For non-static member functions, the type of the implicit object parameter is

— “lvalue reference to cv X” for functions declared without a ref-qualifier or with the & ref-qualifier

— “rvalue reference to cv X” for functions declared with the && ref-qualifier

where X is the class of which the function is a member and cv is the cv-qualification on the member
function declaration.

So what you are asking is equivalent to the following:

void bar(foo&);
void bar(foo&&);
void bar(const foo&);
void bar(const foo&&);

int main()
{
bar(foo());
}

The expression foo() is a class prvalue.

Secondly, the non-const lvalue reference version is not viable, as a prvalue cannot bind to it.

This leaves us with three viable functions for overload resolution.

Each has a single implicit object parameter (const foo&, foo&& or const foo&&), so we must rank these three to determine the best match.

In all three case it is a directly bound reference binding. This is described in declarators/initialization (8.5.3).

The ranking of the three possible bindings (const foo&, foo&& and const foo&&) is described in 13.3.3.2.3:

Standard conversion sequence S1 is a better conversion sequence than standard conversion sequence S2 if

  • S1 and S2 are reference bindings and neither refers to an implicit object parameter of a non-static member function declared without a ref-qualifier [this exception doesn't apply here, they all have ref-qualifiers], and S1 binds an rvalue reference to an rvalue [a class prvalue is an rvalue] and S2 binds an lvalue reference.

This means that both foo&& and const foo&& are better then const foo&.

  • S1 and S2 are reference bindings, and the types to which the references refer are the same type except for top-level cv-qualifiers, and the type to which the reference initialized by S2 refers is more cv-qualified than the type to which the reference initialized by S1 refers.

This means that foo&& is better than const foo&&.

So Clang is right, and it is a bug in GCC. The overload ranking for foo().bar() is as follows:

struct foo
{
int&& bar() &&; // VIABLE - BEST (1)
int const&& bar() const &&; // VIABLE - (2)
int const& bar() const &; // VIABLE - WORST (3)
int& bar() &; // NOT VIABLE

int _bar;
};

The bug in GCC seems to apply purely to implicit object parameters (with ref-qualifiers), for a normal parameter it seems to get the ranking correct, at least in 4.7.2.

what is the && means at the end of a function

&& at the end of the function mean that the function is chosen when this has r-value value category.

See member functions:

A non-static member function can be declared with no ref-qualifier, with an lvalue ref-qualifier (the token & after the parameter list) or the rvalue ref-qualifier (the token && after the parameter list). During overload resolution, non-static cv-qualified member function of class X is treated as follows:

no ref-qualifier: the implicit object parameter has type lvalue reference to cv-qualified X and is additionally allowed to bind rvalue implied object argument

lvalue ref-qualifier: the implicit object parameter has type lvalue reference to cv-qualified X

rvalue ref-qualifier: the implicit object parameter has type rvalue reference to cv-qualified X

What's a use case for overloading member functions on reference qualifiers?

In a class that provides reference-getters, ref-qualifier overloading can activate move semantics when extracting from an rvalue. E.g.:

class some_class {
huge_heavy_class hhc;
public:
huge_heavy_class& get() & {
return hhc;
}
huge_heavy_class const& get() const& {
return hhc;
}
huge_heavy_class&& get() && {
return std::move(hhc);
}
};

some_class factory();
auto hhc = factory().get();

This does seem like a lot of effort to invest only to have the shorter syntax

auto hhc = factory().get();

have the same effect as

auto hhc = std::move(factory().get());

EDIT: I found the original proposal paper, it provides three motivating examples:

  1. Constraining operator = to lvalues (TemplateRex's answer)
  2. Enabling move for members (basically this answer)
  3. Constraining operator & to lvalues. I suppose this is sensible to ensure that the "pointee" is more likely to be alive when the "pointer" is eventually dereferenced:
struct S {
T operator &() &;
};

int main() {
S foo;
auto p1 = &foo; // Ok
auto p2 = &S(); // Error
}

Can't say I've ever personally used an operator& overload.

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

Ref-qualification was added in c++11. 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.

Interpretation of access decoration of member functions

The qualifiers have the exact same meaning as if they were the qualifiers on the hypothetical implicit object parameter which is passed the object expression of the member access expression.

So, #4 can not be called on a prvalue, because a non-const lvalue reference can not bind to a prvalue, explaining why A{}.f(); doesn't work. (A{} is a prvalue)

The old style without reference qualifier is the odd one. It behaves in overload resolution as if the implicit object parameter was an lvalue reference (const or not depending on that qualifier), but in contrast to normal function parameters it is allowed to bind to rvalues anyway for the purpose of overload resolution.

So to replicate the old style unqualified behavior, you need to specify both the &-qualified overload and the &&-qualified overload (at least if the function is not also const-qualified). (There are likely some corner cases where the two qualified member functions are not 100% equivalent to one unqualified one though. I guess a simple example would be trying to take the address &B::f.)



Related Topics



Leave a reply



Submit