c++ - smart - ¿Cómo deducir el tipo de retorno de un objeto de función de la lista de parámetros?
programar contratos inteligentes (2)
Estoy tratando de escribir una función de proyección que podría transformar un vector<T>
en un vector<R>
. Aquí hay un ejemplo:
auto v = std::vector<int> {1, 2, 3, 4};
auto r1 = select(v, [](int e){return e*e; }); // {1, 4, 9, 16}
auto r2 = select(v, [](int e){return std::to_string(e); }); // {"1", "2", "3", "4"}
Primer intento:
template<typename T, typename R>
std::vector<R> select(std::vector<T> const & c, std::function<R(T)> s)
{
std::vector<R> v;
std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
return v;
}
Pero para
auto r1 = select(v, [](int e){return e*e; });
Yo obtengo:
error C2660: ''seleccionar'': la función no toma 2 argumentos
Tengo que llamar explícitamente a select<int,int>
para que funcione. No me gusta esto porque los tipos son redundantes.
auto r1 = select<int, int>(v, [](int e){return e*e; }); // OK
Segundo intento:
template<typename T, typename R, typename Selector>
std::vector<R> select(std::vector<T> const & c, Selector s)
{
std::vector<R> v;
std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
return v;
}
El resultado es el mismo error, la función no toma 2 argumentos. En este caso, tengo que proporcionar un argumento de 3er tipo:
auto r1 = select<int, int, std::function<int(int)>>(v, [](int e){return e*e; });
Tercer intento:
template<typename T, typename R, template<typename, typename> class Selector>
std::vector<R> select(std::vector<T> const & c, Selector<T,R> s)
{
std::vector<R> v;
std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
return v;
}
por
auto r1 = select<int, int, std::function<int(int)>>(v, [](int e){return e*e; });
el error es:
''seleccionar'': argumento de plantilla no válido para ''Selector'', plantilla de clase esperada
por
auto r1 = select(v, [](int e){return e*e; });
error C2660: ''seleccionar'': la función no toma 2 argumentos
(Sé que los dos últimos intentos no son particularmente buenos).
¿Cómo puedo escribir esta función de plantilla select()
para que funcione con el código de ejemplo que coloco al principio?
Opción 1:
decltype()
básico de decltype()
:
template <typename T, typename F>
auto select(const std::vector<T>& c, F f)
-> std::vector<decltype(f(c[0]))>
{
using R = decltype(f(c[0]));
std::vector<R> v;
std::transform(std::begin(c), std::end(c), std::back_inserter(v), f);
return v;
}
Opcion 2:
std::result_of<T>
básico std::result_of<T>
:
template <typename T, typename F, typename R = typename std::result_of<F&(T)>::type>
std::vector<R> select(const std::vector<T>& c, F f)
{
std::vector<R> v;
std::transform(std::begin(c), std::end(c), std::back_inserter(v), f);
return v;
}
Opción # 3:
decltype()
avanzado de decltype()
y reenvío perfecto (ver notas *):
template <typename T, typename A, typename F>
auto select(const std::vector<T, A>& c, F&& f)
-> std::vector<typename std::decay<decltype(std::declval<typename std::decay<F>::type&>()(*c.begin()))>::type>
{
using R = typename std::decay<decltype(std::declval<typename std::decay<F>::type&>()(*c.begin()))>::type;
std::vector<R> v;
std::transform(std::begin(c), std::end(c)
, std::back_inserter(v)
, std::forward<F>(f));
return v;
}
Opción # 4:
Advanced std::result_of<T>
uso y reenvío perfecto (ver notas *):
template <typename T, typename A, typename F, typename R = typename std::decay<typename std::result_of<typename std::decay<F>::type&(typename std::vector<T, A>::const_reference)>::type>::type>
std::vector<R> select(const std::vector<T, A>& c, F&& f)
{
std::vector<R> v;
std::transform(std::begin(c), std::end(c)
, std::back_inserter(v)
, std::forward<F>(f));
return v;
}
* Nota: Las opciones # 3 y # 4 asumen que el algoritmo std::transform
toma un valor de función del objeto, y luego lo usa como un valor no constante. Esta es la razón por la que uno puede ver este extraño nombre de typename std::decay<F>::type&
syntax. Si se supone que el objeto de función debe llamarse dentro de la función de select
sí, y el tipo de resultado no se usará como un argumento de plantilla de contenedor (para el propósito de lo que se usa más std::decay<T>
) , entonces la sintaxis correcta y portátil para obtener el tipo de retorno es:
/*#3*/ using R = decltype(std::forward<F>(f)(*c.begin()));
/*#4*/ typename R = typename std::result_of<F&&(typename std::vector<T, A>::const_reference)>::type
Tu primer problema es que piensas que una lambda es una std::function
. A std::function
y a lambda son tipos no relacionados. std::function<R(A...)>
es un objeto de borrado de tipo que puede convertir cualquier cosa que se pueda copiar (A), (B) puede destruirse y (C) se puede invocar usando A...
y devuelve un tipo compatible con R
, y borra toda otra información sobre el tipo.
Esto significa que puede consumir tipos completamente no relacionados, siempre que pasen esas pruebas.
Un lambda es una clase anónima que se puede destruir, se puede copiar (excepto en C ++ 14, donde esto es a veces), y tiene un operator()
que especifique. Esto significa que a menudo se puede convertir un lambda en una std::function
con una firma compatible.
Deducir la std::function
de la lambda no es una buena idea (hay formas de hacerlo, pero son malas ideas: C ++ 14 auto
lambdas auto
rompen, y además obtienes una ineficiencia innecesaria).
Entonces, ¿cómo resolvemos su problema? Como lo veo, su problema es tomar un objeto de función y un contenedor y deducir qué tipo de transform
de elemento produciría después de aplicar el objeto de función en cada elemento, para que pueda almacenar el resultado en un std::vector
.
Esta es la respuesta más cercana a la solución a su problema:
template<typename T, typename R, typename Selector>
std::vector<R> select(std::vector<T> const & c, Selector s) {
std::vector<R> v;
std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
return v;
}
Lo más fácil de hacer es intercambiar T
y R
en el orden de la plantilla, y hacer que la persona que llama pase en R
explícitamente, como select<double>
. Esto deja que se deduzca T
y Selector
. Esto no es ideal, pero hace una pequeña mejora.
Para una solución completa, hay dos formas de abordar la solución de esta solución. Primero, podemos cambiar la select
para devolver un objeto temporal con un operator std::vector<R>
, retrasando la transformación a ese punto. Aquí hay un boceto incompleto:
template<typename T, typename Selector>
struct select_result {
std::vector<T> const& c;
Selector s;
select_result(select_result&&)=default;
select_result(std::vector<T> const & c_, Selector&& s_):
c(c_), s(std::forward<Selector>(s_)
{}
operator std::vector<R>()&& {
std::vector<R> v;
std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
return v;
}
};
template<typename T, typename Selector>
select_result<T, Selector> select(std::vector<T> const & c, Selector&& s) {
return {c, std::forward<Selector>(s)};
}
También puedo proporcionar una versión más pulida que, lamentablemente, se basa en un comportamiento indefinido (la captura de referencia de referencias locales en una función tiene problemas de por vida según la norma).
Pero eso elimina la sintaxis auto v = select
: termina almacenando lo que produce resultados, en lugar de los resultados.
Aún puede hacer std::vector<double> r = select( in_vec, [](int x){return x*1.5;} );
y funciona bastante bien.
Básicamente, he dividido la deducción en dos fases, una para argumentos y otra para valor de retorno.
Sin embargo, hay poca necesidad de confiar en esa solución, ya que hay otras formas más directas.
Para un segundo enfoque, podemos deducir R
nosotros mismos:
template<typename T, typename Selector>
std::vector<typename std::result_of<Selector(T)>::type>
select(std::vector<T> const & c, Selector s) {
using R = typename std::result_of<Selector(T)>::type;
std::vector<R> v;
std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
return v;
}
que es una solución bastante sólida. Un toque de limpieza:
// std::transform takes by-value, then uses an lvalue:
template<class T>
using decayed_lvalue = typename std::decay<T>::type&;
template<
typename T, typename A,
typename Selector,
typename R=typename std::result_of<decayed_lvalue<Selector>(T)>::type
>
std::vector<R> select(std::vector<T, A> const & c, Selector&& s) {
std::vector<R> v;
std::transform(begin(c), end(c), back_inserter(v), std::forward<Selector>(s));
return v;
}
hace que esta sea una solución útil. (movió R
a template
listas de tipos de template
, permitió asignadores alternativos al vector
, eliminó algunos std::
innecesarios e hizo un reenvío perfecto en el Selector
).
Sin embargo, podemos hacerlo mejor.
El hecho de que la entrada sea un vector
es bastante inútil:
template<
typename Range,
typename Selector,
typename R=typename std::result_of<Selector(T)>::type
>
std::vector<R> select(Range&& in, Selector&& s) {
std::vector<R> v;
using std::begin; using std::end;
std::transform(begin(in), end(in), back_inserter(v), std::forward<Selector>(s));
return v;
}
que no se compila debido a la incapacidad para determinar T
todavía. Así que vamos a trabajar en eso:
namespace details {
namespace adl_aux {
// a namespace where we can do argument dependent lookup on begin and end
using std::begin; using std::end;
// no implementation, just used to help with ADL based decltypes:
template<class R>
decltype( begin( std::declval<R>() ) ) adl_begin(R&&);
template<class R>
decltype( end( std::declval<R>() ) ) adl_end(R&&);
}
// pull them into the details namespace:
using adl_aux::adl_begin;
using adl_aux::adl_end;
}
// two aliases. The first takes a Range or Container, and gives
// you the iterator type:
template<class Range>
using iterator = decltype( details::adl_begin( std::declval<Range&>() ) );
// the second is syntactic sugar on top of `std::iterator_traits`:
template<class Iterator>
using value_type = typename std::iterator_traits<Iterator>::value_type;
lo que nos da un iterator<Range>
y value_type<Iterator>
alias. Juntos nos dejan deducir T
fácilmente:
// std::transform takes by-value, then uses an lvalue:
template<class T>
using decayed_lvalue = typename std::decay<T>::type&;
template<
typename Range,
typename Selector,
typename T=value_type<iterator<Range&>>,
typename R=typename std::result_of<decayed_lvalue<Selector>(T)>::type
>
std::vector<R> select(Range&& in, Selector&& s) {
std::vector<R> v;
using std::begin; using std::end;
std::transform(begin(in), end(in), back_inserter(v), std::forward<Selector>(s));
return v;
}
Y Bob es tu tío . ( decayed_lvalue
refleja cómo se usa el tipo de Selector
para casos de esquina, y el iterator<Range&>
refleja que estamos obteniendo un iterador de la versión lvalue de Range
).
En VS2013, a veces, los decltype
anteriores confunden la implementación decltype
de C ++ 11 que tienen. Reemplazar el iterator<Range>
con decltype(details::adl_begin(std::declval<Range>()))
tan feo como sea que puede solucionar ese problema.
// std::transform takes by-value, then uses an lvalue:
template<class T>
using decayed_lvalue = typename std::decay<T>::type&;
template<
typename Range,
typename Selector,
typename T=value_type<decltype(details::adl_begin(std::declval<Range&>()))>,
typename R=typename std::result_of<decayed_lvalue<Selector>(T)>::type
>
std::vector<R> select(Range&& in, Selector&& s) {
std::vector<R> v;
using std::begin; using std::end;
std::transform(begin(in), end(in), back_inserter(v), std::forward<Selector>(s));
return v;
}
La función resultante tomará matrices, vectores, listas, mapas o contenedores escritos personalizados, tomará cualquier función de transformación y producirá un vector del tipo resultante.
El siguiente paso es hacer que la transformación sea perezosa en lugar de ponerla directamente en un vector
. Puede tener as_vector
que toma un rango y lo escribe en un vector si necesita deshacerse de la evaluación perezosa. Pero eso es escribir una biblioteca completa en lugar de resolver su problema.