avakar
avakar

Reputation: 32685

Why is this overload of a conversion operator chosen?

Consider the following code.

struct any
{
    template <typename T>
    operator T &&() const;

    template <typename T>
    operator T &() const;
};
int main()
{
    int a = any{};
}

Here the second conversion operator is chosen by the overload resolution. Why?

As far as I understand it, the two operators are deduced to operator int &&() const and operator int &() const respectively. Both are in the set of viable functions. Reading through [over.match.best] didn't help me figure out why the latter is better.

Why is the latter function better than the former?

Upvotes: 27

Views: 832

Answers (3)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275976

The deduced return value conversion operators are a bit strange. But the core idea is that it acts like a function parameter to pick which one is used.

And when deciding between T&& and T& the T& wins in the overload resolution rules. This is to permit:

template<class T>
void f( T&& ) { std::cout << "rvalue"; }
template<class T>
void f( T& ) { std::cout << "lvalue"; }

to work. T&& can match against an lvalue, but when both the lvalue and universal reference overloads are available, the lvalue one is preferred.

The right set of conversion operators is probably:

template <typename T>
operator T&&() &&;

template <typename T>
operator T &() const; // maybe &

or even

template <typename T>
operator T() &&;

template <typename T>
operator T &() const; // maybe &

to prevent failed lifetime extension from biting you.

3 The types used to determine the ordering depend on the context in which the partial ordering is done:

[SNIP]

(3.2) In the context of a call to a conversion function, the return types of the conversion function templates are used.

Which then ends up depending on "more specialized" rules when picking overloads:

(9.1) if the type from the argument template was an lvalue reference and the type from the parameter template was not, the parameter type is not considered to be at least as specialized as the argument type; otherwise,

Thus operator T&& is not at least as specialized as operator T&, meanwhile no rule states operator T& is not at least as specialized as operator T&&, so operator T& is more specialized than operator T&&.

More specialized templates win overload resolution over less, everything else being equal.

Upvotes: 12

Brian Bi
Brian Bi

Reputation: 119641

The conversion operator that returns T& is preferred because it is more specialized than the conversion operator that returns T&&.

See C++17 [temp.deduct.partial]/(3.2):

In the context of a call to a conversion function, the return types of the conversion function templates are used.

and /9:

If, for a given type, deduction succeeds in both directions (i.e., the types are identical after the transformations above) and both P and A were reference types (before being replaced with the type referred to above): — if the type from the argument template was an lvalue reference and the type from the parameter template was not, the parameter type is not considered to be at least as specialized as the argument type; ...

Upvotes: 13

Barry
Barry

Reputation: 304182

We're trying to initialize an int from an any. The process for that it:

  1. Figure out all the way we can do that. That is, determine all of our candidates. These come from the non-explicit conversion functions that can be converted to int via a standard conversion sequence ([over.match.conv]). The section contains this phrase:

    A call to a conversion function returning “reference to X” is a glvalue of type X, and such a conversion function is therefore considered to yield X for this process of selecting candidate functions.

  2. Pick the best candidate.

After step 1, we have two candidates. operator int&() const and operator int&&() const, both of which are considered to yield int for the purposes of selecting candidate functions. Which is the best candidate yielding int?

We do have a tiebreaker that prefers lvalue references to rvalue references ([over.ics.rank]/3.2.3). We're not really binding a reference here though, and the example there is somewhat inverted - this is for the case where the parameter is an lvalue vs rvalue reference.

If that doesn't apply, then we fall through to the [over.match.best]/2.5 tiebreaker of preferring the more specialized function template.

Generally speaking, the rule of thumb is the more specific conversion is the best match. The lvalue reference conversion function is more specific than the forwarding reference conversion function, so it's preferred. There's nothing about the int we're initializing that requires an rvalue (had we instead been initializing an int&&, then the operator T&() const would not have been a candidate).

Upvotes: 4

Related Questions