Resolución de sobrecarga para el operador heredado de multiply()


Primero, considere este código 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 espera de acuerdo con el Estándar, esto no se compila, porque print se considera por separado en cada clase base para el propósito de la resolución de sobrecarga, y por lo tanto las llamadas son ambiguas. Este es el caso de Clang (4.0), gcc (6.3) y MSVC (17.0) - ver resultados de godbolt aquí.

Ahora considere el siguiente fragmento, cuya única diferencia es que usamos operator() en 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");
}

Me gustaría espere que los resultados sean idénticos al caso anterior, pero no es el caso - mientras gcc todavía se queja, Clang y MSVC pueden compilar esta multa!

Pregunta # 1: ¿quién tiene razón en este caso? Espero que sea gcc, pero el hecho de que otros dos compiladores no relacionados den 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 sintaxis de función.

También tenga en cuenta que si solo descomenta una de las declaraciones using, pero no la otra, entonces los tres compiladores fallarán al compilar, porque solo considerarán la función introducida por 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 a ello 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 de un tipo lambda. De nuevo, esperarías que los resultados sean consistentes con el fragmento anterior; y esto es cierto para el caso en que ambas declaraciones using están comentadas, o si ambas no están comentadas. Pero si solo comentas uno pero no el otro, las cosas son de repente diferentes de nuevo: 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 using!

Pregunta # 2: ¿quién tiene razón en este caso? De nuevo, esperaría que fuera MSVC, pero entonces ¿por qué Clang y gcc no están de acuerdo? Y, lo que es más importante, ¿por qué esto es diferente del fragmento anterior? Esperaría que el tipo lambda se comporte exactamente igual que un tipo definido manualmente con operator() sobrecargado...

Author: bolov, 2017-06-08

3 answers

[6] Barry tiene la razón # 1. Su #2 golpeó un caso de esquina: las lambdas no genéricas sin captureless tienen una conversión implícita a puntero de función, que se usó en el caso de desajuste. 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 a un puntero a la función y llamándolo. Si compila en -O0 puede ver la llamada a la función de conversión en el ensamblado antes de que se optimice. Ponga un init-capture en print_str, y verá un error desde la conversión implícita desaparece.

Para más, ver [terminado.llamada.objeto].

 7
Author: T.C.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-08 22:37:23

La regla para buscar nombres en las clases base de una clase C solo sucede si C no contiene directamente el nombre es [clase.miembro.búsqueda]/6:

Los siguientes pasos definen el resultado de fusionar lookup set S(f,Bi) en el intermedio S(f,C):

  • Si cada uno de los miembros del subobjeto de S (f, Bi) es un subobjeto de clase base de al menos uno de los miembros del subobjeto de S(f,C), o si S (f,Bi) está vacío, S (f,C) no completo. 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 declaración de S(f,Bi) y S(f, C) difieren,la combinación es ambigua: el nuevo S(f, C) es un conjunto de búsqueda con un conjunto de declaración no válido y la unión de los conjuntos de subobjetos. En las fusiones posteriores, se considera un conjunto de declaraciones inválido 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 declare el mismo nombre, que la clase derivada no traiga una declaración de uso, la búsqueda de ese nombre en la clase derivada se ejecutaría en conflicto con ese segundo punto y la búsqueda debería fallar. Todos sus ejemplos son básicamente los mismos en este respecto.

Pregunta # 1: ¿quién tiene razón en este caso?

Gcc es correcto. La única diferencia entre print y operator() es el nombre que estamos buscando.

Pregunta # 2: ¿quién tiene razón en este caso?

Esta es la misma pregunta que #1 - excepto que tenemos lambdas (que le dan tipos de clase sin nombre con 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, esto es error 58820.

 4
Author: Barry,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-08 13:14:29

Su análisis del primer código es incorrecto. No hay resolución de sobrecarga.

El proceso name lookup ocurre completamente antes de la resolución de sobrecarga. La búsqueda de nombres determina en qué ámbito se resuelve una expresión id .

Si se encuentra un ámbito único a través de las reglas de búsqueda de nombres, entonces comienza la resolución de sobrecarga: todas las instancias de ese nombre dentro de ese ámbito forman el conjunto de sobrecarga.

Pero en su código, la búsqueda de nombres falla. El name no se declara en foo, por lo que se buscan 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 nombres no tienen casos especiales para operadores sobrecargados. Usted debe 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() será una función de barra no estática con un número arbitrario de parámetros. Puede tener argumentos predeterminados. Implementa la sintaxis de llamada a la función

Postfix-expression (expression-list opt )

Donde la expresión postfix se evalúa como un objeto de clase y la expresión posiblemente vacía-list coincide con la lista de parámetros de una función miembro operator() de la clase. Por lo tanto, una llamada x(arg1,...) es interpretado como x.operator()(arg1, ...) para un objeto de clase x de tipo T si existe T::operator()(T1, T2, T3) y si el operador es seleccionado como la función de mejor coincidencia por el mecanismo de resolución de sobrecarga (13.3.3).

Esto en realidad me parece una especificación imprecisa para que pueda entender diferentes compiladores que salen con diferentes resultados. ¿Qué es T1, T2, T3? ¿Se refiere a los tipos de argumentos? (Sospecho que no). ¿Qué son T1, T2, T3 cuando existen múltiples funciones operator(), tomando solo un argumento?

Y ¿qué se entiende por "si T::operator() existe" de todos modos? Tal vez podría significar cualquiera de los siguientes:

  1. operator() se declara en T.
  2. La búsqueda no calificada de operator() en el ámbito de T tiene éxito y la resolución de sobrecarga en ese conjunto de búsqueda con los argumentos dados tiene éxito.
  3. La búsqueda calificada 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.
  4. Algo ¿else?

Para proceder desde aquí (para mí de todos modos) me gustaría entender por qué el estándar no dijo simplemente que f(123) significa f.operator()(123);, el primero está mal formado si y solo si el segundo está mal formado. La motivación detrás de la redacción real podría revelar la intención y, por lo tanto, qué comportamiento del compilador coincide con la intención.

 0
Author: M.M,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-08 14:03:09