Por que std :: function não participa da resolução de sobrecarga?

17

Eu sei que o código a seguir não será compilado.

void baz(int i) { }
void baz() {  }


class Bar
{
    std::function<void()> bazFn;
public:
    Bar(std::function<void()> fun = baz) : bazFn(fun){}

};

int main(int argc, char **argv)
{
    Bar b;
    return 0;
}

Porque std::functioné dito para não considerar a resolução de sobrecarga, como eu li em neste outro post .

Não compreendo completamente as limitações técnicas que forçaram esse tipo de solução.

Eu li sobre as fases da tradução e dos modelos na cppreference, mas não consigo pensar em nenhum raciocínio para o qual não encontrei um contra-exemplo. Explicado a um meio-leigo (ainda novo no C ++), o que e durante qual estágio da tradução faz com que o acima falhe na compilação?

TuRtoise
fonte
11
@ Evg, mas há apenas uma sobrecarga que faria sentido ser aceita no exemplo de OPs. No seu exemplo, ambos fariam uma correspondência
idclev 463035818

Respostas:

13

Isso realmente não tem nada a ver com "fases da tradução". É puramente sobre os construtores de std::function.

Veja, std::function<R(Args)>não requer que a função fornecida seja exatamente do tipo R(Args). Em particular, não requer que seja dado um ponteiro de função. Pode levar qualquer tipo de chamada (ponteiro de função de membro, algum objeto que tenha uma sobrecarga de operator()), desde que seja invocável como se tivesse Argsparâmetros e retornasse algo conversível para R (ou seR forvoid , pode retornar qualquer coisa).

Para fazer isso, o construtor apropriado de std::functiondeve ser um modelo :template<typename F> function(F f); . Ou seja, ele pode aceitar qualquer tipo de função (sujeito às restrições acima).

A expressão bazrepresenta um conjunto de sobrecargas. Se você usar essa expressão para chamar o conjunto de sobrecarga, tudo bem. Se você usar essa expressão como parâmetro para uma função que usa um ponteiro de função específico, o C ++ poderá reduzir a sobrecarga definida em uma única chamada, tornando-a adequada.

No entanto, quando uma função é um modelo e você está usando a dedução de argumento de modelo para descobrir qual é esse parâmetro, o C ++ não tem mais a capacidade de determinar qual é a sobrecarga correta no conjunto de sobrecargas. Então você deve especificá-lo diretamente.

Nicol Bolas
fonte
Perdoe-me se eu entendi algo errado, mas como o C ++ não tem capacidade de distinguir entre as sobrecargas disponíveis? Vejo agora que std :: function <T> aceita qualquer tipo compatível, não apenas uma correspondência exata, mas apenas uma dessas duas sobrecargas baz () é invocável como se tivesse parâmetros especificados. Por que é impossível desambiguar?
TuRtoise
Porque tudo que o C ++ vê é a assinatura que citei. Ele não sabe com o que deve corresponder. Essencialmente, sempre que você usa um conjunto de sobrecarga contra qualquer coisa que não seja óbvia qual é a resposta correta explicitamente do código C ++ (o código é a declaração do modelo), a linguagem o força a soletrar o que você quer dizer.
Nicol Bolas
11
@TuRtoise: O parâmetro do modelo na functionclasse é irrelevante . O que importa é o parâmetro do modelo no construtor que você está chamando. Qual é apenas typename F: aka, qualquer tipo.
Nicol Bolas
11
@TuRtoise: Eu acho que você está entendendo algo errado. É "apenas F" porque é assim que os modelos funcionam. A partir da assinatura, esse construtor de funções usa qualquer tipo, portanto, qualquer tentativa de chamá-lo com dedução de argumento de modelo deduzirá o tipo do parâmetro Qualquer tentativa de aplicar dedução a um conjunto de sobrecargas é um erro de compilação. O compilador não tenta deduzir todos os tipos possíveis do conjunto para ver quais funcionam.
Nicol Bolas
11
"Qualquer tentativa de aplicar dedução a um conjunto de sobrecarga é um erro de compilação. O compilador não tenta deduzir todos os tipos possíveis do conjunto para ver quais funcionam". Exatamente o que estava faltando, obrigado :) #
TuRtoise
7

A resolução de sobrecarga ocorre apenas quando (a) você está chamando o nome de uma função / operador ou (b) está convertendo-o em um ponteiro (para função ou função membro) com uma assinatura explícita.

Nem está ocorrendo aqui.

std::functionpega qualquer objeto que seja compatível com sua assinatura. Não é necessário um ponteiro de função especificamente. (uma lambda não é uma função std e uma função std não é uma lambda)

Agora, nas minhas variantes de função de homebrew, para assinatura R(Args...) , também aceito umR(*)(Args...) argumento (uma correspondência exata) exatamente por esse motivo. Mas isso significa que eleva as assinaturas de "correspondência exata" acima das assinaturas "compatíveis".

O principal problema é que um conjunto de sobrecarga não é um objeto C ++. Você pode nomear um conjunto de sobrecarga, mas não pode transmiti-lo "nativamente".

Agora, você pode criar um conjunto de pseudo-sobrecarga de uma função como esta:

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  -> decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }

#define OVERLOADS_OF(...) \
  [](auto&&...args) \
  RETURNS( __VA_ARGS__(decltype(args)(args)...) )

isso cria um único objeto C ++ que pode sobrecarregar a resolução em um nome de função.

Expandindo as macros, obtemos:

[](auto&&...args)
noexcept(noexcept( baz(decltype(args)(args)...) ) )
-> decltype( baz(decltype(args)(args)...) )
{ return baz(decltype(args)(args)...); }

o que é chato escrever. Uma versão mais simples, apenas um pouco menos útil, está aqui:

[](auto&&...args)->decltype(auto)
{ return baz(decltype(args)(args)...); }

temos uma lambda que aceita qualquer número de argumentos e, em seguida, os encaminha para a perfeição baz.

Então:

class Bar {
  std::function<void()> bazFn;
public:
  Bar(std::function<void()> fun = OVERLOADS_OF(baz)) : bazFn(fun){}
};

trabalho. Adiamos a resolução de sobrecarga para o lambda em que armazenamos fun, em vez de passarfun diretamente um conjunto de sobrecargas (que não pode ser resolvido).

Houve pelo menos uma proposta para definir uma operação na linguagem C ++ que converte um nome de função em um objeto de conjunto de sobrecarga. Até que uma proposta desse padrão esteja no padrão, a OVERLOADS_OFmacro é útil.

Você poderia dar um passo adiante e oferecer suporte ao ponteiro de função de conversão para compatível.

struct baz_overloads {
  template<class...Ts>
  auto operator()(Ts&&...ts)const
  RETURNS( baz(std::forward<Ts>(ts)...) );

  template<class R, class...Args>
  using fptr = R(*)(Args...);
  //TODO: SFINAE-friendly support
  template<class R, class...Ts>
  operator fptr<R,Ts...>() const {
    return [](Ts...ts)->R { return baz(std::forward<Ts>(ts)...); };
  }
};

mas isso está começando a ficar obtuso.

Exemplo ao vivo .

#define OVERLOADS_T(...) \
  struct { \
    template<class...Ts> \
    auto operator()(Ts&&...ts)const \
    RETURNS( __VA_ARGS__(std::forward<Ts>(ts)...) ); \
\
    template<class R, class...Args> \
    using fptr = R(*)(Args...); \
\
    template<class R, class...Ts> \
    operator fptr<R,Ts...>() const { \
      return [](Ts...ts)->R { return __VA_ARGS__(std::forward<Ts>(ts)...); }; \
    } \
  }
Yakk - Adam Nevraumont
fonte
5

A questão aqui é que não há nada dizendo ao compilador como executar a função de deterioração do ponteiro. Se você tem

void baz(int i) { }
void baz() {  }

class Bar
{
    void (*bazFn)();
public:
    Bar(void(*fun)() = baz) : bazFn(fun){}

};

int main(int argc, char **argv)
{
    Bar b;
    return 0;
}

Então o código funcionaria, já que agora o compilador sabe qual função você deseja, pois existe um tipo concreto ao qual você está atribuindo.

Quando você usa, std::functionvocê chama o construtor de objetos de função que tem a forma

template< class F >
function( F f );

e como é um modelo, ele precisa deduzir o tipo do objeto que é passado. Como bazé uma função sobrecarregada, não há um tipo único que possa ser deduzido, portanto a dedução do modelo falha e você recebe um erro. Você teria que usar

Bar(std::function<void()> fun = (void(*)())baz) : bazFn(fun){}

para forçar um único tipo e permitir a dedução.

NathanOliver
fonte
"como baz é uma função sobrecarregada, não há um tipo único que possa ser deduzido", mas como C ++ 14 "esse construtor não participa da resolução de sobrecarga, a menos que f seja Callable para os tipos de argumento Args ... e retorne o tipo R" Estou não tenho certeza, mas eu estava quase esperando que isso fosse suficiente para resolver a ambiguidade
# idclev 463035818 13/12/1919
3
@ formerlyknownas_463035818 Mas, para determinar isso, ele precisa deduzir um tipo primeiro e não pode, pois é um nome sobrecarregado.
NathanOliver
1

No momento, o compilador está decidindo qual sobrecarga deve passar para o std::functionconstrutor. Tudo o que sabe é que o std::functionconstrutor é modelado para usar qualquer tipo. Ele não tem a capacidade de tentar as duas sobrecargas e descobrir que o primeiro não compila, mas o segundo.

A maneira de resolver isso é informar explicitamente ao compilador qual sobrecarga você deseja com um static_cast:

Bar(std::function<void()> fun = static_cast<void(*)()>(baz)) : bazFn(fun){}
Alan Birtles
fonte