Operador de transmisión diferente llamado por diferentes compiladores

80

Considere el siguiente programa corto de C ++:

#include <iostream>

class B {
public:
    operator bool() const {
        return false;
    }
};

class B2 : public B {
public:
    operator int() {
        return 5;
    }
};

int main() {
    B2 b;
    std::cout << std::boolalpha << (bool)b << std::endl;
}

Si lo compilo en diferentes compiladores, obtengo varios resultados. Con Clang 3.4 y GCC 4.4.7 imprime true, mientras que Visual Studio 2013 imprime false, lo que significa que llaman a diferentes operadores de transmisión en (bool)b. ¿Cuál es el comportamiento correcto según el estándar?

En mi entendimiento operator bool()no necesita ninguna conversión, mientras que operator int()requeriría una inta boolla conversión, por lo que el compilador debe elegir la primera. ¿Hace constalgo con eso? ¿El compilador considera la conversión constante más "cara"?

Si elimino el const, todos los compiladores producen igualmente falsecomo salida. Por otro lado, si combino las dos clases juntas (ambos operadores estarán en la misma clase), los tres compiladores producirán trueresultados.

buc
fuente
3
Según tengo entendido, B2 tiene 2 operadores: B :: operator bool () y B2 :: operator int (). Ambos operadores no son operadores const y hay una diferencia entre las versiones const y no const. Puede agregar la versión const del bool en B y la versión const del int en B2 y ver los cambios.
Tanuki
1
constes la clave aquí. Ambos operadores sean igualmente constantes o no, se comportan como se esperaba, la boolversión "gana". Con diferente consistencia, siempre la versión no constante "gana" en cada plataforma. Con clases derivadas y diferente consistencia de los operadores, VS parece tener un error ya que se comporta de manera diferente a los otros dos compiladores.
buc
¿Podrían los diferentes constructores predeterminados ser responsables del comportamiento diferente?
Idgorman
16
EDG también imprime true. Si GCC, Clang y EDG están de acuerdo y MSVC no está de acuerdo, eso generalmente significa que MSVC está equivocado.
Jonathan Wakely
1
Nota: en general en C ++, el tipo de retorno no participa en la resolución de sobrecargas.
Matthieu M.

Respuestas:

51

Los estados estándar:

Una función de conversión en una clase derivada no oculta una función de conversión en una clase base a menos que las dos funciones se conviertan al mismo tipo.

§12.3 [class.conv]

Lo que significa que operator boolno está oculto por operator int.

Los estados estándar:

Durante la resolución de sobrecarga, el argumento del objeto implícito es indistinguible de otros argumentos.

§13.3.3.1 [over.match.funcs]

El "argumento de objeto implícito" en este caso es b, que es de tipo B2 &. operator boolrequiere const B2 &, por lo que el compilador tendrá que agregar const bpara llamar operator bool. Esto, en igualdad de condiciones, hace operator intuna mejor combinación.

El estándar establece que un static_cast(que el reparto de estilo C está realizando en este caso) puede convertirse en un tipo T(en este caso int) si:

la declaración T t(e);está bien formada, para alguna variable temporal inventadat .

§5.2.9 [expr.static.cast]

Por lo tanto, intse puede convertir a a bool, y a boolse puede convertir igualmente en a bool.

Los estados estándar:

Se consideran las funciones de conversión de Sy sus clases base. Aquellas funciones de conversión no explícitas que no están ocultas dentro Sy producen tipo T o un tipo que se puede convertir a tipo a Ttravés de una secuencia de conversión estándar son funciones candidatas.

§13.3.1.5 [over.match.conv]

Entonces, el conjunto de sobrecarga consta de operator inty operator bool. En igualdad de condiciones, operator intes una mejor combinación (ya que no tiene que agregar constness). Por operator intlo tanto, debe seleccionarse.

Tenga en cuenta que (quizás en contra de la intuición) el estándar no considera el tipo de retorno (es decir, el tipo al que se convierten estos operadores) una vez que se han agregado al conjunto de sobrecarga (como se estableció anteriormente), siempre que la secuencia de conversión para los argumentos de uno de ellos es superior a la secuencia de conversión para los argumentos del otro (que, debido a la constancia, es el caso en este caso).

Los estados estándar:

Dadas estas definiciones, una función viable F1 se define como una función mejor que otra función viable F2 si para todos los argumentos i, ICSi (F1) no es una secuencia de conversión peor que ICSi (F2), y luego

  • para algún argumento j, ICSj (F1) es una secuencia de conversión mejor que ICSj (F2), o, si no es así,
  • el contexto es una inicialización por conversión definida por el usuario y la secuencia de conversión estándar del tipo de retorno de F1 al tipo de destino (es decir, el tipo de la entidad que se inicializa) es una secuencia de conversión mejor que la secuencia de conversión estándar del tipo de retorno de F2 al tipo de destino.

§13.3.3 [over.match.best]

En este caso, solo hay un argumento (el thisparámetro implícito ). La secuencia de conversión para B2 &=> B2 &(llamar operator int) es superior a B2 &=> const B2 &(llamar operator bool) y, por operator intlo tanto, se selecciona del conjunto de sobrecarga sin tener en cuenta el hecho de que en realidad no se convierte directamente en bool.

Robert Allan Hennigan Leahy
fuente
2
Puede obtener N3337, que fue (creo) el primer borrador después de C ++ 11, y solo incluye cambios muy, muy menores aquí de forma gratuita. En cuanto a encontrar la parte adecuada del estándar, generalmente se trata de juzgar las secciones del PDF, aprender dónde están las cosas (buscando las cosas / citando el estándar) y buscar palabras clave relevantes con CTRL + F.
Robert Allan Hennigan Leahy
1
@ikh ¿Dónde encuentro los documentos estándar actuales de C o C ++? es la mejor pregunta para esto y, por lo que sé, cubre todos los borradores que están disponibles tanto para C como para C ++.
Shafik Yaghmour
2
Eso es lo que dije con "solo se aplica después de determinar qué secuencia de conversión es mejor". Sin embargo, creo que esta es una parte importante de la respuesta: hay (en teoría) un conflicto entre la conversión del argumento de objeto implícito y la "conversión del tipo de retorno", y se resuelve de manera inequívoca mediante los diferentes pasos de resolución de sobrecarga. [over.match.best] /1.4 señala claramente la separación de esos pasos y por qué a la secuencia de conversión estándar final no le importa en este caso.
dyp
2
Edité la respuesta para incorporar su sugerencia, explicando por qué el tipo de retorno de estos operadores de conversión no se considera al seleccionar el "mejor".
Robert Allan Hennigan Leahy
2
@RobertAllanHenniganLeahy Dado que ese era el punto de la pregunta, ¿no debería aparecer en algún lugar de la respuesta, no solo un comentario?
Barmar
9

Corto

La función de conversión operator int()se selecciona mediante clang over operator bool() constya bque no está calificado const, mientras que el operador de conversión para bool sí lo es.

El corto razonamiento es que las funciones candidatas para la resolución de sobrecarga (con parámetro de objeto implícita en su lugar), al convertir ba boolson

operator bool (B2 const &);
operator int (B2 &);

donde el segundo es un mejor partido ya bque no está calificado const.

Si ambas funciones comparten la misma calificación (ambas consto no), operator boolse selecciona ya que proporciona conversión directa.

Conversión mediante notación emitida, analizada paso a paso

Si estamos de acuerdo en que el inserter booleano ostream (std :: basic_ostream :: operator << (bool val) según [ostream.inserters.arithmetic]) se llama con el valor que resulta de una conversión de ba bool, podemos profundizar en esa conversión .

1. La expresión del elenco

El elenco de b to bool

(bool)b

evalúa a

static_cast<bool>(b)

según C ++ 11, 5.4 / 4 [expr.cast] desdeconst_cast que no es aplicable (no se agrega ni se elimina const aquí).

Esta conversión estática está permitida por C ++ 11, 5.2.9 / 4 [expr.static.cast] , si bool t(b);para una variable inventada t está bien formado. Tales declaraciones se denominan inicialización directa según C ++ 11, 8.5 / 15 [dcl.init] .

2. Inicialización directa bool t(b);

La cláusula 16 del párrafo estándar menos mencionado establece (el énfasis es mío):

La semántica de los inicializadores es la siguiente. El tipo de destino es el tipo de objeto o referencia que se inicializa y el tipo de origen es el tipo de expresión inicializadora.

[...]

[...] si el tipo de fuente es un tipo de clase (posiblemente calificado por cv) , funciones de conversión se consideran las .

Se enumeran las funciones de conversión aplicables y se elige la mejor mediante resolución de sobrecarga.

2.1 ¿Qué funciones de conversión están disponibles?

Las funciones de conversión disponibles son operator int ()y operator bool() constcomo C ++ 11, 12.3 / 5 [class.conv] nos dice:

Una función de conversión en una clase derivada no oculta una función de conversión en una clase base a menos que las dos funciones se conviertan al mismo tipo.

Mientras que C ++ 11, 13.3.1.5/1 [over.match.conv] dice:

Se consideran las funciones de conversión de S y sus clases base.

donde S es la clase desde la que se convertirá.

2.2 ¿Qué funciones de conversión son aplicables?

C ++ 11, 13.3.1.5/1 [over.match.conv] (el énfasis es mío):

1 [...] Suponiendo que "cv1 T" es el tipo de objeto que se está inicializando, y "cv S" es el tipo de expresión inicializadora, con S un tipo de clase, las funciones candidatas se seleccionan de la siguiente manera: La conversión Se consideran las funciones de S y sus clases base. Las funciones de conversión no explícitas que no están ocultas dentro de S y producen el tipo T o un tipo que se puede convertir al tipo T a través de una secuencia de conversión estándar son funciones candidatas.

Por operator bool () constlo tanto, es aplicable ya que no se esconde dentro B2y produce a bool.

La parte con énfasis en la última cotización estándar es relevante para la conversión usando operator int ()ya que intes un tipo que se puede convertir a bool mediante la secuencia de conversión estándar. La conversión de inta boolni siquiera es una secuencia, sino una conversión directa simple que está permitida por C ++ 11, 4.12 / 1 [conv.bool]

Un prvalue de aritmética, enumeración sin ámbito, puntero o puntero a tipo de miembro se puede convertir en un prvalue de tipo bool. Un valor cero, un valor de puntero nulo o un valor de puntero de miembro nulo se convierte en falso; cualquier otro valor se convierte en verdadero.

Esto significa que operator int () es aplicable.

2.3 ¿Qué función de conversión se selecciona?

La selección de la función de conversión adecuada se realiza mediante resolución de sobrecarga ( C ++ 11, 13.3.1.5/1 [over.match.conv] ):

La resolución de sobrecarga se utiliza para seleccionar la función de conversión que se invocará.

Hay una "peculiaridad" especial cuando se trata de la resolución de sobrecargas para funciones miembro de clase: el parámetro de objeto implícito ".

Según C ++ 11, 13.3.1 [over.match.funcs] ,

[...] las funciones miembro estáticas y no estáticas tienen un parámetro de objeto implícito [...]

donde el tipo de este parámetro para funciones miembro no estáticas, de acuerdo con la cláusula 4, es:

  • "Referencia de lvalue a cv X" para funciones declaradas sin un calificador ref o con el calificador & ref

  • "Referencia rvalue a cv X" para funciones declaradas con el calificador && ref

donde X es la clase de la que la función es miembro y cv es la calificación cv en la declaración de función miembro.

Esto significa que (según C ++ 11, 13.3.1.5/2 [over.match.conv] ), en una inicialización por función de conversión,

[l] a lista de argumentos tiene un argumento, que es la expresión inicializadora. [Nota: Este argumento se comparará con el parámetro de objeto implícito de las funciones de conversión. —Nota final]

Las funciones candidatas para la resolución de sobrecargas son:

operator bool (B2 const &);
operator int (B2 &);

Obviamente, operator int ()es una mejor coincidencia si se solicita una conversión utilizando un objeto de tipo no constante B2ya queoperator bool () requiere una conversión de calificación.

Si ambas funciones de conversión comparten la misma calificación const, la resolución de sobrecarga de esas funciones ya no funcionará. En este caso, entra en juego la clasificación de conversión (secuencia).

3. ¿Por qué se operator bool ()selecciona cuando ambas funciones de conversión comparten la misma calificación constante?

La conversión de B2a booles una secuencia de conversión definida por el usuario ( C ++ 11, 13.3.3.1.2 / 1 [over.ics.user] )

Una secuencia de conversión definida por el usuario consta de una secuencia de conversión estándar inicial seguida de una conversión definida por el usuario seguida de una segunda secuencia de conversión estándar.

[...] Si la conversión definida por el usuario se especifica mediante una función de conversión, la secuencia de conversión estándar inicial convierte el tipo de fuente en el parámetro de objeto implícito de la función de conversión.

C ++ 11, 13.3.3.2/3 [over.ics.rank]

[...] define un orden parcial de secuencias de conversión implícitas basadas en las relaciones mejor secuencia de conversión y mejor conversión.

[...] La secuencia de conversión definida por el usuario U1 es una secuencia de conversión mejor que otra secuencia de conversión definida por el usuario U2 si contienen la misma función de conversión definida por el usuario o constructor o inicialización agregada y la segunda secuencia de conversión estándar de U1 es mejor que la segunda secuencia de conversión estándar de U2.

La segunda conversión estándar es el caso de operator bool()es boola bool(conversión de identidad) mientras que la segunda conversión estándar en el caso de operator int ()es intabool es una conversión booleana.

Por lo tanto, la secuencia de conversión, usando operator bool (), es mejor si ambas funciones de conversión comparten la misma calificación constante.

Pixelchemist
fuente
En la mayoría de los casos, es bastante intuitivo que lo que haga con el resultado no afecte la forma en que se calcula. La forma en que calculamos a/bes la misma si el código es float f = a/b;o int f = a/b;. Pero este es un caso en el que puede resultar un poco sorprendente.
David Schwartz
1

El tipo bool de C ++ tiene dos valores: verdadero y falso con los valores correspondientes 1 y 0. La confusión inherente se puede evitar si agrega un operador bool en la clase B2 que llama al operador bool de la clase base (B) explícitamente, entonces la salida viene como falso. Aquí está mi programa modificado. Entonces operador bool significa operador bool y no operador int de ninguna manera.

#include <iostream>

class B {
public:
    operator bool() const {
        return false;
    }
};

class B2 : public B {
public:
    operator int() {
        return 5;
    }
    operator bool() {
        return B::operator bool();
    }
};

int main() {
    B2 b;
    std::cout << std::boolalpha << (bool)b << std::endl;
}

En su ejemplo, (bool) b estaba tratando de llamar al operador bool para B2, B2 ha heredado el operador bool y el operador int, por regla de dominio, se llama al operador int y al operador bool heredado en B2. Sin embargo, al tener explícitamente un operador bool en la propia clase B2, el problema se resuelve.

Dr. Debasish Jana
fuente
8
Es una buena solución para escribir un programa, pero no responde en detalle por qué ocurre esto extraño.
ikh
4
true and false with corresponding values 1 and 0Eso es una simplificación excesiva. trueconvierte a entero 1, pero eso no significa que "tiene" el valor 1. De hecho, truepuede estar 42debajo.
Lightness Races in Orbit
Tener un operador bool explícito en B2 evita la confusión y las dependencias del compilador de lo que se llama en B2, operador int u operador bool.
Dr. Debasish Jana
No hay dependencia del compilador. El Estándar requiere que 0 se convierta en falso y 1 se convierta en verdadero.
Puppy
1
@LightnessRacesinOrbit Más concretamente, quizás: a boolnunca tiene el valor 0 o 1; los únicos valores que puede tomar son falseytrue (que se convierten en 0 y 1 si boolse convierte en un tipo integral).
James Kanze
0

Algunas de las respuestas anteriores ya brindan mucha información.

Mi contribución es que las "operaciones de conversión" se compilan de manera similar, a las "operaciones sobrecargadas", sugiero hacer una función con un identificador único para cada operación y luego reemplazarla por el operador requerido o conversión.

#include <iostream>

class B {
public:
    bool ToBool() const {
        return false;
    }
};

class B2 : public B {
public:
    int ToInt() {
        return 5;
    }
};

int main() {
    B2 b;
    std::cout << std::boolalpha << b.ToBool() << std::endl;
}

Y, posteriormente, aplicar el operador o yeso.

#include <iostream>

class B {
public:
    operator bool() {
        return false;
    }
};

class B2 : public B {
public:
    operator int() {
        return 5;
    }
};

int main() {
    B2 b;
    std::cout << std::boolalpha << (bool)b << std::endl;
}

Solo mis 2 centavos.

umlcat
fuente