Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MSVC error C2593 when overloading const and non-const conversion operator returning array type

Recently, I tried using a conversion operator as an alternative to operator [].

Like the code below:

#include <iostream>

class foo
{
public:
    using type = int[1];

public:
    operator type       &()       { return data; }
    operator type const &() const { return data; }

private:
    type data;
};

int main()
{
    foo f;
    f[0] = 1;                        // error happens here
    std::cout << f[0] << std::endl;
    return 0;
}

I found it works in G++ but not in MSVCv141(2017).

MSVC reports:

error C2593: 'operator [' is ambiguous
note: could be 'built-in C++ operator[(foo::type, int)'
note: or       'built-in C++ operator[(const foo::type, int)'
note: while trying to match the argument list '(foo, int)'

So is that a bug of MSVC or something else? And how to work around?

like image 567
MYLS Avatar asked Jan 14 '18 14:01

MYLS


1 Answers

This is an MSVC bug. The candidates aren't ambiguous. The work-around I guess would be to either provide a named conversion function to array, or just provide a user-defined operator[].


When using operators, we have [over.match.oper]:

If either operand has a type that is a class or an enumeration, a user-defined operator function might be declared that implements this operator or a user-defined conversion can be necessary to convert the operand to a type that is appropriate for a built-in operator.

In f[0], f has class type, so we consider either the user-defined operator functions or user-defined conversions. There are none of the former, but two of the latter. Both are viable candidates:

f.operator type&()[1];       // ok
f.operator type const&()[1]; // also ok

So we have to perform overload resolution to pick the best viable candidate. The different between the two is the implicit object parameter. As per [over.match.funcs]:

So that argument and parameter lists are comparable within this heterogeneous set, a member function is considered to have an extra parameter, called the implicit object parameter, which represents the object for which the member function has been called. For the purposes of overload resolution, both static and non-static member functions have an implicit object parameter, but constructors do not. [ ... ]

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

[ ... ] where X is the class of which the function is a member and cv is the cv-qualification on the member function declaration. [ ... ] For conversion functions, the function is considered to be a member of the class of the implied object argument for the purpose of defining the type of the implicit object parameter.

So overload resolution here behaves as if we had:

type&       __op(foo& );       // #1
type const& __op(foo const& ); // #2

__op(f);

At which point, the ICS ranking rules tell us that:

Standard conversion sequence S1 is a better conversion sequence than standard conversion sequence S2 if [ ... ] 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.

The reference binding in #1 is less cv-qualified than the reference binding in #2, so it's a better match. Since it's a better match, we do end up with a unique, best viable function, and the call is well-formed.


Moreover, having the conversion operators return pointers or references to arrays shouldn't matter here. The underlying rules are the same, but MSVC allows the former. And MSVC also allows this:

struct  foo
{
    using type = int[1];
    operator type&       ()       { return data; }
    operator type const& () const { return data; }
    type data;
};

void call(int (&)[1]); // #1
void call(int const (&)[1]); // #2

int main()
{
    foo f;
    call(f); // correctly calls #1
}
like image 165
Barry Avatar answered Oct 17 '22 06:10

Barry