c++ - todas - ¿Cuáles son las reglas de conversión de tipo para parámetros y valores de retorno de las lambdas?
tipos de parametros en c++ (4)
La conversión de una referencia a un valor me parece extraña.
¿Por qué?
¿Esto parece extraño también?
int foo(int i) { return i; }
void bar(const int& ir) { foo(ir); }
Esto es exactamente lo mismo. Una función que toma un int
por valor es llamada por otra función, que toma un int
por una referencia constante.
Dentro de la bar
la variable ir
se copia, y el valor de retorno se ignora. Eso es exactamente lo que sucede dentro de std::function<void(const int&)>
cuando tiene un objetivo con la firma int(int)
.
Recientemente me sorprendió el hecho de que las lambdas se pueden asignar a std::function
s con firmas ligeramente diferentes . Un significado ligeramente diferente de que los valores de retorno de la lambda se pueden ignorar cuando se especifica que la function
devuelve un void
, o que los parámetros pueden ser referencias en la function
pero valores en la lambda.
Vea este ejemplo (ideone) donde resalté lo que sospecharía que es incompatible. Creo que el valor de retorno no es un problema, ya que siempre se puede llamar a una función e ignorar el valor de retorno, pero la conversión de una referencia a un valor me parece extraña:
int main() {
function<void(const int& i)> f;
// ^^^^ ^^^^^ ^
f = [](int i) -> int { cout<<i<<endl; return i; };
// ^^^ ^^^^^^
f(2);
return 0;
}
La pregunta menor es: ¿Por qué este código se compila y funciona? La pregunta principal es: ¿Cuáles son las reglas generales para la conversión de tipo de parámetros lambda y valores de retorno cuando se usan junto con std::function
?
El operador de asignación se define para tener el efecto:
function(std::forward<F>(f)).swap(*this);
( 14882: 2011 20.8.11.2.1 par. 18 )
El constructor que esta referencia,
template <class F> function(F f);
requiere:
F
seráCopyConstructible
.f
seráCallable
(20.8.11.2) para el tipo de argumentoArgTypes
y el tipo de retornoR
donde Callable
se define como sigue:
Un objeto llamable
f
de tipoF
esCallable
para los tipos de argumentoArgTypes
y devuelve el tipoR
si la expresiónINVOKE(f, declval<ArgTypes>()..., R)
, considerada como un operando no evaluado (Cláusula 5), está bien formada (20.8.2).
INVOKE
, a su vez, se define como :
Defina INVOKE (f, t1, t2, ..., tN) de la siguiente manera:
- ... casos de manejo de funciones miembro omitidas aquí ...
f(t1, t2, ..., tN)
en todos los demás casos.Defina
INVOKE(f, t1, t2, ..., tN, R)
comostatic_cast<void>(INVOKE(f, t1, t2, ..., tN))
siR
es cvvoid
, de lo contrarioINVOKE(f, t1, t2, ..., tN)
implícitamente convertido aR
Dado que la definición de INVOKE
se convierte en una llamada de función simple, en este caso, los argumentos se pueden converted : Si su std::function<void(const int&)>
acepta una const int&
luego se puede convertir en un int
para la llamada. El siguiente ejemplo compila con clang++ -std=c++14 -stdlib=libc++ -Wall -Wconversion
:
int main() {
std::function<void(const int& i)> f;
f = [](int i) -> void { std::cout << i << std::endl; };
f(2);
return 0;
}
El tipo de devolución ( void
) se maneja mediante el static_cast<void>
especial static_cast<void>
para la definición INVOKE
.
Sin embargo, clang++ -std=c++1z -stdlib=libc++ -Wconversion -Wall
en cuenta que al momento de escribir lo siguiente se genera un error cuando se compila con clang++ -std=c++1z -stdlib=libc++ -Wconversion -Wall
, pero no cuando se compila con clang++ -std=c++1z -stdlib=libstdc++ -Wconversion -Wall
:
int main() {
std::function<void(const int& i)> f;
f = [](int i) -> int { std::cout << i << std::endl; return i;};
f(2);
return 0;
}
Esto se debe a que libc++
implementa el comportamiento como se especifica en C ++ 14, en lugar del cplusplus.github.io/LWG/lwg-defects.html#2420 descrito anteriormente (gracias a @Jonathan Wakely por señalarlo). @Arunmu describe el rasgo de tipo libstdc++
responsable de lo mismo en su publicación . En este sentido, las implementaciones pueden comportarse de manera ligeramente diferente cuando se manejan callables con tipos de retorno void
, dependiendo de si implementan C ++ 11, 14 o algo más nuevo.
Puede asignar un lambda a un objeto de función de tipo std::function<R(ArgTypes...)>
cuando el lambda es Lvalue-Callable para esa firma. A su vez, Lvalue-Callable se define en términos de la operación INVOKE , que aquí significa que la lambda tiene que ser invocable cuando es un lvalue y todos sus argumentos son valores de los tipos y categorías de valor requeridos (como si cada argumento fuera el valor). resultado de llamar a una función nula con ese tipo de argumento como el tipo de retorno en su firma).
Es decir, si le das a tu lambda una identificación para que podamos referirnos a su tipo,
auto l = [](int i) -> int { cout<<i<<endl; return i; };
Para asignarlo a una function<void(const int&)>
, la expresión
static_cast<void>(std::declval<decltype(l)&>()(std::declval<const int&>()))
debe estar bien formado.
El resultado de std::declval<const int&>()
es una referencia de lvalue a const int
, pero no hay problema en vincular eso con el argumento int
, ya que esta es solo la conversión de lvalue a rvalue , que se considera una coincidencia exacta Para los propósitos de resolución de sobrecarga:
return l(static_cast<int const&>(int{}));
Como se ha observado, los valores de retorno se descartan si la firma del objeto de función tiene un tipo de retorno void
; de lo contrario, los tipos de retorno tienen que ser implícitamente convertibles. Como señala Jonathan Wakely, C ++ 11 tuvo un comportamiento insatisfactorio en esto ( usar `std :: function <void (...)>` para llamar a una función no-vacía ; ¿es ilegal invocar una función std :: function <void (¿Args ...)> bajo el estándar? ), Pero se ha corregido desde entonces, en cplusplus.github.io/LWG/lwg-defects.html#2420 ; La resolución se aplicó como una corrección posterior a la publicación de C ++ 14. La mayoría del compilador de C ++ moderno proporcionará el comportamiento de C ++ 14 (como enmendado) como una extensión, incluso en el modo C ++ 11.
Simplemente agregando a la respuesta de @ecatmur, g ++ / libstd ++ simplemente eligió ignorar el valor de retorno del callable
, es tan normal como uno haría ignorar el valor de retorno en el código regular:
static void
_M_invoke(const _Any_data& __functor, _ArgTypes&&... __args)
{
(*_Base::_M_get_pointer(__functor))( // Gets the pointer to the callable
std::forward<_ArgTypes>(__args)...);
}
El rasgo de tipo que explícitamente permite esto en libstd ++ es:
template<typename _From, typename _To>
using __check_func_return_type = __or_<is_void<_To>, is_convertible<_From, _To>>;