Resolución de sobrecarga ambigua en el puntero de función y std :: function para una lambda usando +

93

En el siguiente código, la primera llamada a fooes ambigua y, por lo tanto, no se puede compilar.

El segundo, con el agregado +antes de la lambda, se resuelve en la sobrecarga del puntero de función.

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

¿Qué hace la +notación aquí?

Steve Lorimer
fuente

Respuestas:

98

El +en la expresión +[](){}es el +operador unario . Se define de la siguiente manera en [expr.unary.op] / 7:

El operando del +operador unario debe tener enumeración aritmética, sin ámbito o tipo puntero y el resultado es el valor del argumento.

La lambda no es de tipo aritmético, etc., pero se puede convertir:

[expr.prim.lambda] / 3

El tipo de expresión lambda [...] es un tipo de clase sin unión, sin nombre, único, llamado tipo de cierre , cuyas propiedades se describen a continuación.

[expr.prim.lambda] / 6

El tipo de cierre para un lambda-expresión sin lambda de captura tiene un publicno- virtualno- explicit constfunción de conversión a puntero a la función que tienen los mismos parámetros y valores de retorno como operador de llamada de función del tipo de cierre. El valor devuelto por esta función de conversión será la dirección de una función que, cuando se invoca, tiene el mismo efecto que la invocación del operador de llamada de función del tipo de cierre.

Por lo tanto, el unario +fuerza la conversión al tipo de puntero de función, que es para esta lambda void (*)(). Por lo tanto, el tipo de expresión +[](){}es este tipo de puntero de función void (*)().

La segunda sobrecarga se void foo(void (*f)())convierte en una coincidencia exacta en la clasificación de resolución de sobrecarga y, por lo tanto, se elige sin ambigüedades (ya que la primera sobrecarga NO es una coincidencia exacta).


La lambda [](){}se puede convertir a std::function<void()>través de la plantilla no explícita ctor de std::function, que toma cualquier tipo que cumpla con los requisitos Callabley CopyConstructible.

La lambda también se puede convertir a void (*)()través de la función de conversión del tipo de cierre (ver arriba).

Ambas son secuencias de conversión definidas por el usuario y del mismo rango. Es por eso que la resolución de sobrecarga falla en el primer ejemplo debido a la ambigüedad.


Según Cassio Neri, respaldado por un argumento de Daniel Krügler, este +truco unario debe ser un comportamiento específico, es decir, puede confiar en él (ver discusión en los comentarios).

Aún así, recomendaría usar una conversión explícita al tipo de puntero de función si desea evitar la ambigüedad: no necesita preguntarle a SO qué hace y por qué funciona;)

dyp
fuente
3
Los punteros de función miembro de @Fred AFAIK no se pueden convertir en punteros de función no miembro, y mucho menos en valores de función. Puede vincular una función miembro a través std::bindde un std::functionobjeto que se puede llamar de manera similar a una función lvalue.
dyp
2
@DyP: Creo que podemos confiar en lo complicado. De hecho, suponga que una implementación se suma operator +()a un tipo de cierre sin estado. Suponga que este operador devuelve algo diferente al puntero a la función a la que se convierte el tipo de cierre. Entonces, esto alteraría el comportamiento observable de un programa que viola 5.1.2 / 3. Por favor, avíseme si está de acuerdo con este razonamiento.
Cassio Neri
2
@CassioNeri Sí, ese es el punto en el que no estoy seguro. Estoy de acuerdo en que el comportamiento observable podría cambiar al agregar un operator +, pero esto se compara con la situación en la que no existe operator +para empezar. Pero no se especifica que el tipo de cierre no tendrá operator +sobrecarga. "Una implementación puede definir el tipo de cierre de manera diferente a lo que se describe a continuación, siempre que esto no altere el comportamiento observable del programa más que por [...]", pero la OMI agregar un operador no cambia el tipo de cierre a algo diferente de lo que se "describe a continuación".
dyp
3
@DyP: La situación en la que no hay operator +()es exactamente la que describe el estándar. El estándar permite que una implementación haga algo diferente de lo que se especifica. Por ejemplo, agregando operator +(). Sin embargo, si esta diferencia es observable por un programa, entonces es ilegal. Una vez pregunté en comp.lang.c ++. Moderado si un tipo de cierre podría agregar un typedef para result_typey el otro typedefsrequerido para hacerlos adaptables (por ejemplo, por std::not1). Me dijeron que no podía porque esto era observable. Intentaré encontrar el enlace.
Cassio Neri
6
VS15 le da este divertido error: test.cpp (543): error C2593: 'operador +' es ambiguo t \ test.cpp (543): nota: podría ser 'operador C ++ incorporado + (void (__cdecl *) (void )) 't \ test.cpp (543): nota: o' operador C ++ incorporado + (void (__stdcall *) (void)) 't \ test.cpp (543): nota: o' operador C ++ incorporado + (void (__fastcall *) (void)) 't \ test.cpp (543): nota: o' operador C ++ incorporado + (void (__vectorcall *) (void)) 't \ test.cpp (543): nota : al intentar hacer coincidir la lista de argumentos '(wmain :: <lambda_d983319760d11be517b3d48b95b3fe58>) test.cpp (543): error C2088:' + ': ilegal para la clase
Ed Lambert