c++ - Resolución de sobrecarga para el operador heredado múltiple()
lambda language-lawyer (3)
Primero, considere este código de C ++:
#include <stdio.h>
struct foo_int {
void print(int x) {
printf("int %d/n", x);
}
};
struct foo_str {
void print(const char* x) {
printf("str %s/n", x);
}
};
struct foo : foo_int, foo_str {
//using foo_int::print;
//using foo_str::print;
};
int main() {
foo f;
f.print(123);
f.print("abc");
}
Como se esperaba de acuerdo con el estándar, esto no se puede compilar, porque la print
se considera por separado en cada clase base con el fin de resolver la sobrecarga, y por lo tanto las llamadas son ambiguas. Este es el caso de Clang (4.0), gcc (6.3) y MSVC (17.0) - vea los resultados de godbolt aquí .
Ahora considere el siguiente fragmento, la única diferencia es que utilizamos operator()
lugar de print
:
#include <stdio.h>
struct foo_int {
void operator() (int x) {
printf("int %d/n", x);
}
};
struct foo_str {
void operator() (const char* x) {
printf("str %s/n", x);
}
};
struct foo : foo_int, foo_str {
//using foo_int::operator();
//using foo_str::operator();
};
int main() {
foo f;
f(123);
f("abc");
}
Esperaría que los resultados sean idénticos al caso anterior, pero no es el caso , mientras que gcc aún se queja, ¡Clang y MSVC pueden compilar esto bien!
Pregunta # 1: ¿quién es el correcto en este caso? Espero que sea gcc, pero el hecho de que otros dos compiladores no relacionados brinden un resultado consistentemente diferente aquí me hace preguntarme si me falta algo en el estándar, y las cosas son diferentes para los operadores cuando no se invocan usando la sintaxis de la función.
También tenga en cuenta que si solo descomenta una de las declaraciones de using
, pero no la otra, entonces los tres compiladores no compilarán, ya que solo considerarán la función introducida al using
durante la resolución de sobrecarga y, por lo tanto, una de las llamadas fallará debido a la falta de coincidencia de tipos. Recuerda esto; volveremos sobre eso más tarde.
Ahora considere el siguiente código:
#include <stdio.h>
auto print_int = [](int x) {
printf("int %d/n", x);
};
typedef decltype(print_int) foo_int;
auto print_str = [](const char* x) {
printf("str %s/n", x);
};
typedef decltype(print_str) foo_str;
struct foo : foo_int, foo_str {
//using foo_int::operator();
//using foo_str::operator();
foo(): foo_int(print_int), foo_str(print_str) {}
};
int main() {
foo f;
f(123);
f("foo");
}
De nuevo, igual que antes, excepto que ahora no definimos operator()
explícitamente, sino que logramos obtenerlo de un tipo lambda. De nuevo, esperaría que los resultados fueran consistentes con el fragmento anterior; y esto es cierto para el caso en que ambas declaraciones de using
están comentadas , o si ambas están sin comentario . Pero si solo comenta uno pero no el otro, las cosas cambian repentinamente de repente : ahora solo MSVC se queja como yo esperaría, mientras que Clang y gcc piensan que está bien, y usan ambos miembros heredados para la resolución de sobrecarga, a pesar de que solo uno ¡ser traído using
!
Pregunta # 2: ¿quién es el correcto en este caso? Nuevamente, espero que sea MSVC, pero ¿por qué no están de acuerdo tanto Clang como GCC? Y, lo que es más importante, ¿por qué esto es diferente del fragmento anterior? Esperaría que el tipo lambda se comportara exactamente igual que un tipo definido manualmente con el operator()
sobrecargado ...
Barry obtuvo el # 1 correcto. Tu # 2 golpea una esquina: las lambdas no genéricas sin captura tienen una conversión implícita al puntero de función, que se usó en el caso de falta de coincidencia. Es decir, dado
struct foo : foo_int, foo_str {
using foo_int::operator();
//using foo_str::operator();
foo(): foo_int(print_int), foo_str(print_str) {}
} f;
using fptr_str = void(*)(const char*);
f("hello")
es equivalente a f.operator fptr_str()("hello")
, convirtiendo el foo
en un puntero a función y llamando a eso. Si compila en -O0
, puede ver la llamada a la función de conversión en el ensamblado antes de que se optimice. Ponga una captura de print_str
en print_str
, y verá un error ya que la conversión implícita desaparece.
Para obtener más información, consulte [over.call.object] .
La regla para la búsqueda de nombres en clases base de una clase C
solo ocurre si C
no contiene directamente el nombre es [class.member.lookup] / 6 :
Los siguientes pasos definen el resultado de combinar el conjunto de búsqueda
S(f,Bi)
en el intermedioS(f,C)
:
Si cada uno de los miembros subobjeto de S (f, Bi) es un subobjeto de clase base de al menos uno de los miembros subobjeto de S (f, C), o si S (f, Bi) está vacío, S (f, C ) no se modifica y la fusión está completa. Por el contrario, si cada uno de los miembros del subobjeto de S (f, C) es un subobjeto de clase base de al menos uno de los miembros del subobjeto de S (f, Bi), o si S (f, C) está vacío, el nuevo S (f, C) es una copia de S (f, Bi).
De lo contrario, si los conjuntos de declaraciones de S (f, Bi) y S (f, C) difieren, la fusión es ambigua : el nuevo S (f, C) es un conjunto de búsqueda con un conjunto de declaraciones inválido y la unión del subobjeto conjuntos. En las fusiones posteriores, un conjunto de declaración no válido se considera diferente de cualquier otro.
De lo contrario, el nuevo S (f, C) es un conjunto de búsqueda con el conjunto compartido de declaraciones y la unión de los conjuntos de subobjetos.
Si tenemos dos clases base, que cada una declara el mismo nombre, que la clase derivada no trae con una declaración de uso, la búsqueda de ese nombre en la clase derivada entraría en conflicto con ese segundo punto y la búsqueda debería fallar. Todos sus ejemplos son básicamente los mismos a este respecto.
Pregunta # 1: ¿quién es el correcto en este caso?
gcc es correcto. La única diferencia entre print
y operator()
es el nombre que estamos buscando.
Pregunta # 2: ¿quién es el correcto en este caso?
Esta es la misma pregunta que # 1, excepto que tenemos lambdas (que le dan tipos de clase sin nombre con el operator()
sobrecarga operator()
) en lugar de tipos de clase explícitos. El código debe estar mal formado por la misma razón. Al menos para gcc, este es el error 58820 .
Su análisis del primer código es incorrecto. No hay resolución de sobrecarga
El proceso de búsqueda de nombre ocurre completamente antes de la resolución de sobrecarga. La búsqueda de nombres determina a qué ámbito se resuelve una expresión-id .
Si se encuentra un alcance único a través de las reglas de búsqueda de nombre, entonces comienza la resolución de sobrecarga: todas las instancias de ese nombre dentro de ese alcance forman el conjunto de sobrecarga.
Pero en su código, la búsqueda de nombre falla. El nombre no está declarado en foo
, por lo que se buscan las clases base. Si el nombre se encuentra en más de una clase base inmediata, entonces el programa está mal formado y el mensaje de error lo describe como un nombre ambiguo.
Las reglas de búsqueda de nombre no tienen casos especiales para operadores sobrecargados. Debes encontrar que el código:
f.operator()(123);
falla por la misma razón que f.print
falló. Sin embargo, hay otro problema en su segundo código. f(123)
NO se define como siempre significando f.operator()(123);
. De hecho, la definición en C ++ 14 está en [over.call]:
operator()
debe ser una función miembro no estática con un número arbitrario de parámetros. Puede tener argumentos predeterminados. Implementa la sintaxis de llamada de funciónpostfix-expression (lista de expresión opt)
donde la expresión de postfijo se evalúa como un objeto de clase y la lista de expresiones posiblemente vacía coincide con la lista de parámetros de una función miembro de
operator()
de la clase. Por lo tanto, una llamadax(arg1,...)
se interpreta comox.operator()(arg1, ...)
para un objeto de clase x de tipo T siT::operator()(T1, T2, T3)
existe y si el operador es seleccionado como la mejor función de coincidencia por el mecanismo de resolución de sobrecarga (13.3.3).
Esto realmente me parece una especificación imprecisa, así que puedo entender diferentes compiladores que salen con diferentes resultados. ¿Qué es T1, T2, T3? ¿Significa los tipos de los argumentos? (Sospecho que no). ¿Qué son T1, T2, T3 cuando existe la función operator()
múltiple operator()
, solo tomando un argumento?
¿Y qué significa "si T::operator()
existe" de todos modos? Quizás podría significar cualquiera de los siguientes:
-
operator()
se declara enT
- La búsqueda no calificada del
operator()
en el alcance deT
tiene éxito y la resolución de sobrecarga en ese conjunto de búsqueda con los argumentos dados tiene éxito. - La búsqueda cualificada de
T::operator()
en el contexto de llamada tiene éxito y la resolución de sobrecarga en ese conjunto de búsqueda con los argumentos dados tiene éxito. - ¿Algo más?
Para continuar desde aquí (para mí de todos modos), me gustaría entender por qué el estándar no decía simplemente que f(123)
significa f.operator()(123);
, el primero siendo mal formado si y solo si este último está mal formado. La motivación detrás de la redacción actual podría revelar la intención y, por lo tanto, qué comportamiento del compilador coincide con la intención.