Mala deducción de tipo al pasar el puntero de función sobrecargado y sus argumentos

8

Estoy tratando de proporcionar un contenedor std::invokepara hacer el trabajo de deducir el tipo de función incluso cuando la función está sobrecargada.
(Hice una pregunta relacionada ayer para la versión de puntero variadic y método).

Cuando la función tiene un argumento, este código (C ++ 17) funciona como se espera en condiciones normales de sobrecarga:

#include <functional>

template <typename ReturnType, typename ... Args>
using FunctionType = ReturnType (*)(Args...);

template <typename S, typename T>
auto Invoke (FunctionType<S, T> func, T arg)
{   
    return std::invoke(func, arg);
}

template <typename S, typename T>
auto Invoke (FunctionType<S, T&> func, T & arg)
{   
    return std::invoke(func, arg);
}

template <typename S, typename T>
auto Invoke (FunctionType<S, const T&> func, const T & arg)
{
    return std::invoke(func, arg);
}

template <typename S, typename T>
auto Invoke (FunctionType<S, T&&> func, T && arg)
{   
    return std::invoke(func, std::move(arg));
}

Obviamente, es necesario reducir la acumulación de código para obtener más argumentos de entrada, pero ese es un problema separado.

Si el usuario tiene sobrecargas que difieren solo por constantes / referencias, así:

#include <iostream>

void Foo (int &)
{
    std::cout << "(int &)" << std::endl;
}

void Foo (const int &)
{
    std::cout << "(const int &)" << std::endl;
}

void Foo (int &&)
{
    std::cout << "(int &&)" << std::endl;
}

int main()
{
    int num;
    Foo(num);
    Invoke(&Foo, num);

    std::cout << std::endl;

    Foo(0);
    Invoke(&Foo, 0);
}

Luego Invokededuce la función incorrectamente, con salida de g ++:

(int &)
(const int &)

(int &&)
(const int &)

Y clang ++:

(int &)
(const int &)

(int &&)
(int &&)

(Gracias a geza por señalar que los resultados de clang fueron diferentes).

Entonces Invoketiene un comportamiento indefinido.

Sospecho que la metaprogramación sería la forma de abordar este problema. De todos modos, ¿es posible manejar la deducción de tipo correctamente en el Invokesitio?

Elliott
fuente
cual es la salida esperada? ¿Es (int &) (int &&)?
LF
@LF, sí. Esas son las salidas de Foo, por lo que también deberían ser la salida de Invoke.
Elliott
1
Para mí, clang da un resultado diferente: imprime (int &&)dos veces para el segundo caso.
geza
2
Debe tener algo que ver con la Sdeducción de argumentos. Intente comentar la const T &versión de Invokey observe el error. Además, si el argumento se proporciona explícitamente ( Invoke<void>(&Foo, num)), se llama a la versión correcta.
aparpara el
2
Aquí hay una teoría para el primer caso: cuando el compilador considera que no es constante Invoke, puede instanciarla tanto con constante como sin constante Foo. Y no comprueba que el tipo de retorno ( S) sea el mismo para ambos, por lo que dice que no puede deducir S. Entonces ignora esta plantilla. Si bien la creación de instancias de la constante solo Invokese puede hacer con la constante Foo, por lo que se puede deducir Sen este caso. Por lo tanto, el compilador usa esta plantilla.
geza

Respuestas:

1

Teoría

Para cada plantilla de función Invoke, la deducción de argumento de plantilla (que debe tener éxito para que la resolución de sobrecarga lo considere) considera cada una Foo para ver si puede deducir cuántos parámetros de plantilla (aquí, dos) para el parámetro de una función ( func) involucrado. La deducción general solo puede tener éxito si Foocoincide exactamente uno (porque de lo contrario no hay forma de deducirlo S). (Esto fue más o menos declarado en los comentarios).

El primero ("por valor") Invokenunca sobrevive: se puede deducir de cualquiera de los Foos. Del mismo modo, la segunda constsobrecarga ("sin referencia") acepta los dos primeros Foos. Tenga en cuenta que estos se aplican independientemente del otro argumento para Invoke(for arg)!

La tercera const T&sobrecarga ( ) selecciona la Foosobrecarga correspondiente y deduce T= int; el último hace lo mismo con la última sobrecarga (donde T&&es una referencia de valor normal), y por lo tanto rechaza los argumentos de valor a pesar de su referencia universal (que deduce Tcomo int&(o const int&) en ese caso y entra en conflicto con funcla deducción).

Compiladores

Si el argumento para arges un valor r (y, como de costumbre, no es constante), ambas Invokesobrecargas plausibles tienen éxito en la deducción, y la T&&sobrecarga debería ganar (porque vincula una referencia de valor r a un valor r ).

Para el caso de los comentarios:

template <typename U>
void Bar (U &&);
int main() {
  int num;
  Invoke<void>(&Bar, num);
}

No se realiza ninguna deducción, &Barya que se trata de una plantilla de función, por lo que Tse deduce con éxito (as int) en todos los casos. Entonces, la deducción pasa de nuevo para cada caso para identificar la Barespecialización (si lo hay) para su uso, deduciendo Ucomo fallar , int&, const int&, y int&respectivamente. Los int&casos son idénticos y claramente mejores, por lo que la llamada es ambigua.

Entonces Clang está aquí. (Pero aquí no hay un "comportamiento indefinido").

Solución

No tengo una respuesta general para ti; Dado que ciertos tipos de parámetros pueden aceptar múltiples pares de categoría de valor / calificación constante, no será fácil emular la resolución de sobrecarga correctamente en todos estos casos. Ha habido propuestas para reificar conjuntos de sobrecarga de una forma u otra; puede considerar una de las técnicas actuales en ese sentido (como una lambda genérica por nombre de función de destino).

Arenque Davis
fuente