Почему неявное преобразование не является неоднозначным для не примитивных типов?

Дан простой шаблон класса с несколькими неявными функциями преобразования (неявный конструктор и оператор преобразования), как в следующем примере:

template<class T>
class Foo
{
private:
    T m_value;

public:
    Foo();

    Foo(const T& value):
        m_value(value)
    {
    }

    operator T() const {
        return m_value;
    }

    bool operator==(const Foo<T>& other) const {
        return m_value == other.m_value;
    }
};

struct Bar
{
    bool m;

    bool operator==(const Bar& other) const {
        return false;
    }
};

int main(int argc, char *argv[])
{
    Foo<bool> a (true);
    bool b = false;
    if(a == b) {
        // This is ambiguous
    }

    Foo<int> c (1);
    int d = 2;
    if(c == d) {
        // This is ambiguous
    }

    Foo<Bar> e (Bar{true});
    Bar f = {false};
    if(e == f) {
        // This is not ambiguous. Why?
    }
}

Операторы сравнения, включающие примитивные типы (bool, int), как и ожидалось, неоднозначны - компилятор не знает, должен ли он использовать оператор преобразования для преобразования экземпляра левого класса шаблона в примитивный тип или использовать конструктор преобразования для преобразования тип примитива справа к ожидаемому экземпляру шаблона класса.

Однако последнее сравнение, включающее простую struct, не является двусмысленным. Зачем? Какая функция преобразования будет использоваться?

Протестировано с компилятором msvc 15.9.7.

Ответ 1

Согласно [over.binary]/1

Таким образом, для любого бинарного оператора @ [email protected] может интерпретироваться как [email protected](y) или [email protected](x,y).

Согласно этому правилу, в случае e == f, компилятор может интерпретировать его только как e.operator==(f), а не как f.operator==(e). Так что нет никакой двусмысленности; operator== вы определили как член Bar, просто не подходит для разрешения перегрузки.

В случае a == b и c == d встроенный operator==(int, int) -кандидат operator==(int, int) (см. [Over.built]/13) конкурирует с operator== определенным как член Foo<T>.

Ответ 2

Перегрузки операторов, реализованные как функции-члены, не допускают неявного преобразования их левого операнда, который является объектом, для которого они вызываются.

Это всегда помогает выписать явный вызов перегрузки оператора, чтобы лучше понять, что именно он делает:

Foo<Bar> e (Bar{true});
Bar f = {false};

// Pretty explicit: call the member function Foo<Bar>::operator==
if(e.operator ==(f)) { /* ... */ }

Это нельзя спутать с оператором сравнения в Bar, потому что это потребует неявного преобразования левой части, что невозможно.

Вы можете вызвать неоднозначность, аналогичную той, которую вы видите со встроенными типами, когда определяете Bar и его оператор сравнения следующим образом:

struct Bar { bool m; };

// A free function allows conversion, this will be ambiguous:
bool operator==(const Bar&, const Bar&)
{
   return false;
}

Это хорошо продемонстрировано и объяснено в статье "Эффективность Скотта Мейерса" C++, пункт 24.