c++ inheritance syntax ternary-operator

c++ - ¿Por qué puede(falso? A(): B()). Test() compilar solo cuando A y B tienen una relación de subclase?



inheritance syntax (4)

Originalmente me gusta usar algo como esto:

(true?a:b).test()

en lugar de

(true?a.test():b.test())

para ahorrar tiempo de tipeo si la función tiene el mismo nombre, inicialmente pensé que debería ser válida, pero encontré:

#include <stdio.h> class A{ public: char test(){ return ''A''; } }; class B{ public: char test(){ return ''B''; } }; int main(){ printf("%c/n",(false?A():B()).test()); return 0; }

no se puede compilar, pero si B es la subclase de A :

#include <stdio.h> class A{ public: char test(){ return ''A''; } }; class B : public A{ public: char test(){ return ''B''; } }; int main(){ printf("%c/n",(false?A():B()).test()); return 0; }

puede compilar, ¿por qué?


La razón es que (test?a:b) es una expresión y debe tener un tipo. Ese tipo es el tipo común de ayb, y los tipos no relacionados no tienen ningún tipo en común. El tipo común de una clase base y derivada es la clase base.

Tenga en cuenta que la pregunta contiene una suposición de que el único caso que compila es donde hay un tipo de base común. De hecho, también compila si hay una conversión inequívoca de un tipo a otro.


Agrégalo al idioma:

template<class F> struct if_t { bool b; F f; template<class Lhs, class Rhs> auto operator()(Lhs&&lhs, Rhs&&rhs)&& ->std::result_of_t<F(Lhs)> { if (b) return std::forward<F>(f)(std::forward<Lhs>(lhs)); else return std::forward<F>(f)(std::forward<Rhs>(rhs)); } template<class Lhs> void operator()(Lhs&&lhs)&& { if (b) std::forward<F>(f)(std::forward<Lhs>(lhs)); } }; template<class F> if_t<std::decay_t<F>> branch(bool b, F&& f){ return {b,std::forward<F>(f)}; }

entonces obtenemos:

branch(false, [&](auto&&arg){return arg.test();}) ( A{}, B{} );

donde solo funciona si tanto A como B tienen un .test() y el valor de retorno de B::test() se puede convertir en el valor de retorno de A::test() .

Tristemente, tanto A{} como B{} están construidos.

template<class T> auto make(){return [](auto&&...args){return {decltype(args)(args)...};}} branch(false, [&](auto&&arg){return arg().test();}) ( make<A>(), make<B>() );

que difiere la construcción.

No es la sintaxis más elegante. Se puede limpiar un poco, pero no lo suficiente (en mi opinión). No hay forma de crear operadores diferidos en C ++ con una sintaxis limpia, solo puede usar los integrados.

De todos modos, su código no puede funcionar porque ?: Es una expresión que devuelve un tipo. No hay ningún tipo que pueda representar tanto A como B , por lo que no puede funcionar. Si uno era la base del otro, o había una conversión, etc., entonces funcionaría.


Los operandos de operador condicional ( ?: :) Deben tener un tipo común. Es decir, dado E1 ? E2 : E3 E1 ? E2 : E3 entonces E2 y E3 deben ser inequívocamente convertibles. Este tipo es entonces el tipo de retorno que luego se usa para la expresión como un todo.

Desde cppreference , enumeran las reglas y requisitos, pero una línea destacada que es relevante aquí es;

También se puede acceder al tipo de devolución de un operador condicional como el carácter de tipo binario std::common_type

Lo que básicamente dice que debe haber un tipo común, y std::common_type se puede usar para calcular ese tipo.

Según su fragmento de código (true ? a.test() : b.test()) funcionó porque tanto a.test() como b.test() devolvieron char . Sin embargo, b no están relacionados, por lo tanto, no pueden ser utilizados por ellos mismos.

El material relevante en el estándar C ++ ( WD n4527 ) se encuentra en §5.16 ([expr.cond]) . Hay varias reglas y conversiones que se aplican, la esencia de esto es que si no hay conversión, o si la conversión es ambigua, el programa está mal formado .


Si el segundo y tercer operando al operador condicional no tienen el mismo "tipo", entonces se realiza un intento de encubrir uno de los operandos al otro. Si esta conversión no puede hacerse o es ambigua, entonces el programa está mal formado.

Esto tiene lo que para algunos puede parecer resultados inesperados, uno de esos casos interesantes sería lambda sin captura con la firma de función compatible después de la conversión a un puntero de función , por ejemplo:

#include <iostream> #include <algorithm> #include <vector> int main() { std::vector<int> v(0, 10); bool increase = true; std::sort(v.begin(), v.end(), increase ? [](int lhs, int rhs){return lhs < rhs;} : [](int lhs, int rhs){return lhs > rhs;} ); return 0; }

es válida. Podemos ver en el siguiente informe de error que esto también se aplica en general y, en particular, para esta pregunta se aplica a las clases sin una base común. El siguiente ejemplo se toma del informe de errores:

struct A{ typedef void (*F)(); operator F(); }; struct B{ typedef void (*F)(); operator F(); }; void f() { false ? A() : B(); }

es válido ya que al igual que el caso lambda sin captura, tanto A como B se pueden convertir a un puntero de función.

Para referencia, el borrador del estándar de C ++ en la sección 5.16 [expr.condido] dice:

De lo contrario, si el segundo y tercer operando tienen diferentes tipos y tienen un tipo de clase (posiblemente cv calificado), o si ambos son glvalues ​​de la misma categoría de valor y el mismo tipo excepto cv-qualification, se intenta convertir cada uno de esos operandos al tipo del otro.

y luego cubre las reglas y luego dice:

Usando este proceso, se determina si el segundo operando puede convertirse para coincidir con el tercer operando, y si el tercer operando puede convertirse para coincidir con el segundo operando. Si ambos se pueden convertir, o uno se puede convertir, pero la conversión es ambigua, el programa está mal formado. Si ninguno de los dos puede convertirse, los operandos no se modificarán y se realizarán más comprobaciones tal como se describe a continuación. Si es posible exactamente una conversión, esa conversión se aplica al operando elegido y el operando convertido se utiliza en lugar del operando original para el resto de esta sección