ternarios - ¿Por qué esta expresión de C++ que involucra a operadores sobrecargados y conversiones implícitas es ambigua?




sobrecarga de operadores ternarios (3)

operator bool interrumpe el uso de operator< en el siguiente ejemplo. ¿Puede alguien explicar por qué bool es tan relevante en la expresión if (a < 0) como el operador específico, y si existe una solución alternativa?

struct Foo {
    Foo() {}
    Foo(int x) {}

    operator bool() const { return false; }

    friend bool operator<(const Foo& a, const Foo& b) {
        return true;
    }
};

int main() {
    Foo a, b;
    if (a < 0) {
        a = 0;
    }
    return 1;
}

Cuando compilo, obtengo:

g++ foo.cpp
foo.cpp: In function 'int main()':
foo.cpp:18:11: error: ambiguous overload for 'operator<' (operand types are 'Foo' and 'int')
     if (a < 0) {
           ^
foo.cpp:18:11: note: candidate: operator<(int, int) <built-in>
foo.cpp:8:17: note: candidate: bool operator<(const Foo&, const Foo&)
     friend bool operator<(const Foo& a, const Foo& b)

Debido a que el compilador no puede elegir entre el bool operator <(const Foo &,const Foo &) y el operator<(bool, int) ambos encajan en esta situación.

Para solucionar el problema, haga explicit segundo constructor:

struct Foo
{
    Foo() {}
    explicit Foo(int x) {}

    operator bool() const { return false; }

    friend bool operator<(const Foo& a, const Foo& b)
    {
        return true;
    }
};

Edit: Ok, al fin me dieron un punto real de la pregunta :) OP pregunta por qué su compilador ofrece al operator<(int, int) como candidato, aunque "no se permiten conversiones de varios pasos" .

Respuesta: Sí, para llamar al operator<(int, int) objeto a debe ser convertido Foo -> bool -> int . Pero , C ++ Standard en realidad no dice que "las conversiones de varios pasos son ilegales".

§ 12.3.4 [class.conv]

Como mucho, una conversión definida por el usuario (constructor o función de conversión) se aplica implícitamente a un solo valor.

bool to int no es una conversión definida por el usuario , por lo tanto, es legal y el compilador tiene todo el derecho de elegir el operator<(int, int) como candidato.


El problema aquí es que C ++ tiene dos opciones para lidiar con a < 0 expresión a < 0 :

  • Convierta a a bool , y compare el resultado a 0 con el operador incorporado < (una conversión)
  • Convierta 0 a Foo y compare los resultados con < que definió (una conversión)

Ambos enfoques son equivalentes al compilador, por lo que emite un error.

Puede hacer esto explícito eliminando la conversión en el segundo caso:

if (a < Foo(0)) {
    ...
}

Los puntos importantes son:

Primero, hay dos sobrecargas relevantes del operator < .

  • operator <(const Foo&, const Foo&) . El uso de esta sobrecarga requiere una conversión definida por el usuario del literal 0 a Foo usando Foo(int) .
  • operator <(int, int) . El uso de esta sobrecarga requiere convertir Foo a bool con el operator bool() definido por el usuario operator bool() , seguido de una promoción a int (esto es, en la versión estándar, diferente de una conversión, como ha señalado Bo Persson).

La pregunta aquí es: ¿de dónde surge la ambigüedad? Ciertamente, la primera llamada, que requiere solo una conversión definida por el usuario, es más sensata que la segunda, que requiere una conversión definida por el usuario seguida de una promoción.

Pero ese no es el caso. El estándar asigna un rango a cada candidato . Sin embargo, no hay rango para "conversión definida por el usuario seguida de una promoción". Esto tiene el mismo rango que solo usando una conversión definida por el usuario. En pocas palabras (pero de manera informal), la secuencia de clasificación se parece un poco a esto:

  1. coincidencia exacta
  2. (solo) promoción requerida
  3. (solo) se requiere una conversión implícita (incluidas las "no seguras" heredadas de C, como float a int )
  4. se requiere conversión definida por el usuario

(Descargo de responsabilidad: como se mencionó, esto es informal. Se vuelve mucho más complejo cuando se involucran múltiples argumentos, y tampoco mencioné las referencias ni la calificación CV. Esto solo pretende ser una descripción general).

Así que esto, con suerte, explica por qué la llamada es ambigua. Ahora para la parte práctica de cómo solucionar este problema. Casi nunca alguien que proporciona el operator bool() quiere que se use implícitamente en expresiones que involucren aritmética de enteros o comparaciones. En C ++ 98, había soluciones alternativas poco claras, que iban desde std::basic_ios<CharT, Traits>::operator void * hasta versiones "mejoradas" más seguras que incluyen punteros a miembros o tipos privados incompletos. Afortunadamente, C ++ 11 introdujo una forma más legible y consistente de prevenir la promoción de enteros después de los usos implícitos del operator bool() , que es marcar al operador como explicit . Esto eliminará la sobrecarga del operator <(int, int) completo, en lugar de solo "degradarlo".

Como han mencionado otros, también puede marcar el constructor Foo(int) como explícito. Esto tendrá el efecto inverso de eliminar la sobrecarga del operator <(const Foo&, const Foo&) .

Una tercera solución sería proporcionar sobrecargas adicionales, por ejemplo:

  • operator <(int, const Foo&)
  • operator <(const Foo&, int)

Este último, en este ejemplo, se preferirá a las sobrecargas mencionadas anteriormente como una coincidencia exacta, incluso si no se introdujo explicit . Lo mismo vale por ejemplo para

  • operator <(const Foo&, long long)

que se preferiría sobre el operator <(const Foo&, const Foo&) en a < 0 porque su uso requiere solo una promoción.





ambiguous