Resolvendo sobrecarga ambígua no ponteiro de função e std :: function para um lambda usando +


93

No código a seguir, a primeira chamada para fooé ambígua e, portanto, falha ao compilar.

O segundo, com o adicionado +antes do lambda, resolve a sobrecarga do ponteiro de função.

#include <functional>

void foo(std::function<void()> f) { f(); }
void foo(void (*f)()) { f(); }

int main ()
{
    foo(  [](){} ); // ambiguous
    foo( +[](){} ); // not ambiguous (calls the function pointer overload)
}

O que a +notação está fazendo aqui?

Respostas:


98

O +na expressão +[](){}é o +operador unário . É definido da seguinte forma em [expr.unary.op] / 7:

O operando do +operador unário deve ter aritmética, enumeração sem escopo ou tipo de ponteiro e o resultado é o valor do argumento.

O lambda não é do tipo aritmético etc., mas pode ser convertido:

[expr.prim.lambda] / 3

O tipo da expressão lambda [...] é um tipo de classe sem união sem nome único - chamado de tipo de fechamento - cujas propriedades são descritas abaixo.

[expr.prim.lambda] / 6

O tipo de fecho para um lambda-expressão sem lambda-captura tem um publicnão- virtualnão-explicit const função de conversão para ponteiro para a função que tem os mesmos tipos de parâmetros e de retorno como operador chamada de função do tipo de fecho. O valor retornado por esta função de conversão deve obrigatoriamente ser o endereço de uma função que, quando invocada, tem o mesmo efeito que invocar o operador de chamada de função do tipo de encerramento.

Portanto, o unário +força a conversão para o tipo de ponteiro de função, que é para este lambdavoid (*)() . Portanto, o tipo da expressão +[](){}é esse tipo de ponteiro de função void (*)().

A segunda sobrecarga void foo(void (*f)()) se torna uma correspondência exata na classificação para resolução de sobrecarga e, portanto, é escolhida sem ambigüidade (pois a primeira sobrecarga NÃO é uma correspondência exata).


O lambda [](){}pode ser convertido por std::function<void()>meio do modelo não explícito ctor de std::function, que leva qualquer tipo que cumpra oCallableCopyConstructible requisitos e .

O lambda também pode ser convertido por void (*)()meio da função de conversão do tipo de fechamento (veja acima).

Ambos são sequências de conversão definidas pelo usuário e da mesma classificação. É por isso que a resolução de sobrecarga falha no primeiro exemplo devido à ambigüidade.


De acordo com Cassio Neri, apoiado por um argumento de Daniel Krügler, este unário + truque deve ser um comportamento especificado, ou seja, você pode confiar nele (veja a discussão nos comentários).

Ainda assim, eu recomendo usar uma conversão explícita para o tipo de ponteiro de função se você quiser evitar a ambigüidade: você não precisa perguntar no SO o que faz e por que funciona;)


3
Ponteiros de função de membro @Fred AFAIK não podem ser convertidos em ponteiros de função de não membro, muito menos lvalues ​​de função. Você pode vincular uma função de membro por meio std::bindde um std::functionobjeto que pode ser chamado de maneira semelhante a uma função lvalue.
dyp

2
@DyP: Acredito que podemos contar com o complicado. Na verdade, suponha que uma implementação seja adicionada operator +()a um tipo de fechamento sem estado. Suponha que esse operador retorne algo diferente do ponteiro para a função para a qual o tipo de fechamento se converte. Então, isso alteraria o comportamento observável de um programa que viola 5.1.2 / 3. Por favor, deixe-me saber se você concorda com este raciocínio.
Cassio Neri

2
@CassioNeri Sim, esse é o ponto em que não tenho certeza. Concordo que o comportamento observável pode mudar ao adicionar um operator +, mas isso se compara à situação em que não há nenhum operator +para começar. Mas não é especificado que o tipo de fechamento não deve ter uma operator +sobrecarga. "Uma implementação pode definir o tipo de fechamento de maneira diferente do que é descrito abaixo, desde que isso não altere o comportamento observável do programa, exceto por [...]" mas a adição de um operador IMO não altera o tipo de fechamento para algo diferente do que é "descrito abaixo".
dyp

3
@DyP: A situação onde não há operator +()é exatamente a descrita pelo padrão. O padrão permite que uma implementação faça algo diferente do que está especificado. Por exemplo, adicionar operator +(). No entanto, se essa diferença for observada por um programa, ela é ilegal. Certa vez, perguntei em comp.lang.c ++. Moderated se um tipo de encerramento poderia adicionar um typedef para result_typee outro typedefsnecessário para torná-los adaptáveis ​​(por exemplo, por std::not1). Disseram-me que não, porque isso era observável. Vou tentar encontrar o link.
Cassio Neri

6
VS15 dá a você este erro divertido: test.cpp (543): erro C2593: 'operador +' é ambíguo t \ test.cpp (543): nota: poderia ser 'operador C ++ embutido + (void (__cdecl *) (void )) 't \ test.cpp (543): note: ou' operador C ++ embutido + (void (__stdcall *) (void)) 't \ test.cpp (543): note: ou' operador C ++ embutido + (void (__fastcall *) (void)) 't \ test.cpp (543): note: ou' operador C ++ embutido + (void (__vectorcall *) (void)) 't \ test.cpp (543): note : ao tentar combinar a lista de argumentos '(wmain :: <lambda_d983319760d11be517b3d48b95b3fe58>) test.cpp (543): erro C2088:' + ': ilegal para a classe
Ed Lambert
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.