Dedução de tipo incorreto ao passar o ponteiro de função sobrecarregado e seus argumentos

8

Estou tentando fornecer um invólucro std::invokepara deduzir o tipo de função mesmo quando a função está sobrecarregada.
(Fiz uma pergunta relacionada ontem para a versão do ponteiro variável e do método).

Quando a função possui um argumento, esse código (C ++ 17) funciona conforme o esperado em condições normais 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));
}

Reduzir o inchaço do código é obviamente necessário para mais argumentos de entrada, mas esse é um problema separado.

Se o usuário tiver sobrecargas diferentes apenas de const / reference, da seguinte forma:

#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);
}

Em seguida, Invokededuz a função incorretamente, com saída g ++:

(int &)
(const & int)

(int &&)
(const & int)

E clang ++:

(int &)
(const & int)

(int &&)
(int &&)

(Obrigado a Geza por apontar que os resultados do clang eram diferentes).

Então, Invoketem um comportamento indefinido.

Suspeito que a metaprogramação seja a maneira de abordar esse problema. Independentemente disso, é possível lidar com a dedução de tipo corretamente no Invokesite?

Elliott
fonte
Qual é o resultado esperado? É (int &) (int &&)?
LF
@LF, sim. Essas são as saídas do Foo, então elas também devem ser a saída do Invoke.
Elliott
1
Para mim, clang fornece um resultado diferente: imprime (int &&)duas vezes no segundo caso.
geza 8/03
2
Deve ter algo a ver com Sdedução de argumento. Tente comentar a const T &versão Invokee observe o erro. Além disso, se o argumento for fornecido explicitamente (Invoke<void>(&Foo, num) ), a versão correta será chamada.
aparpara 08/03
2
Aqui está uma teoria para o primeiro caso: quando o compilador considera o não-const Invoke, ele pode instancia-lo com o const e o não-const Foo. E não verifica se o tipo de retorno ( S) é o mesmo para os dois, portanto diz que não pode deduzir S. Portanto, ele ignora este modelo. Embora a instanciação da const Invokepossa ser feita apenas com a const Foo, é possível deduzirS nesse caso. Portanto, o compilador usa esse modelo.
geza 8/03

Respostas:

1

Teoria

Para cada modelo de função Invoke, a dedução do argumento do modelo (que deve ter êxito na resolução da sobrecarga para considerá-lo) considera cada um Foo para ver se é possível deduzir no entanto muitos parâmetros do modelo (aqui, dois) para o parâmetro de uma função ( func) envolvido. A dedução geral pode ser bem-sucedida apenas se exatamente Foocorresponder (porque, caso contrário, não há como deduzir S). (Isso foi mais ou menos declarado nos comentários.)

O primeiro ("por valor") Invoke nunca sobrevive: pode deduzir de qualquer um dos Foos. Da mesma forma, a segunda constsobrecarga (“não referência”) aceita os dois primeiros Foos. Observe que eles se aplicam independentemente do outro argumento para Invoke(para arg)!

A terceira const T&sobrecarga ( ) seleciona a Foosobrecarga correspondente e deduzT = int; o último faz o mesmo com a última sobrecarga (onde T&&é uma referência normal de valor) e, portanto, rejeita argumentos de valor apesar de sua referência universal (que deduz Tcomo int&(ou const int&) nesse caso e entra em conflito com funca dedução de).

Compiladores

Se o argumento for argfor um rvalue (e, como sempre, não for const), ambas as Invokesobrecargas plausíveis terão êxito na dedução e a T&&sobrecarga deverá vencer (porque vincula uma referência de rvalue a um rvalue ).

Para o caso dos comentários:

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

Nenhuma dedução ocorre &Bardesde que um modelo de função está envolvido; portanto, Té deduzido com êxito (as int) em todos os casos. Então, a dedução acontece novamente para cada caso para identificar a Barespecialização (se houver) para uso, deduzindo Ucomo falhar , int&, const int&, e int&, respectivamente. Os int&casos são idênticos e claramente melhores, portanto a ligação é ambígua.

Então Clang está bem aqui. (Mas não há "comportamento indefinido" aqui.)

Solução

Eu não tenho uma resposta geral para você; como certos tipos de parâmetros podem aceitar vários pares de categoria de valor / qualificação de const, não será fácil emular corretamente a resolução de sobrecarga em todos esses casos. Houve propostas para reificar conjuntos de sobrecarga de uma maneira ou de outra; você pode considerar uma das técnicas atuais nesse sentido (como um lambda genérico por nome da função de destino).

Davis Herring
fonte