opcional - ¿Cuándo debería usar la deducción automática del tipo de devolución C++ 14?
deducción ciega para arrendamiento 2017 (6)
C ++ 11 plantea preguntas similares: cuándo usar la deducción del tipo de retorno en lambdas, y cuándo usar auto
variables auto
.
La respuesta tradicional a la pregunta en C y C ++ 03 ha sido "a través de los límites de las declaraciones, hacemos los tipos explícitos, dentro de las expresiones, por lo general, están implícitas, pero podemos hacerlas explícitas con los moldes". C ++ 11 y C ++ 1y introducen herramientas de deducción de tipo para que pueda omitir el tipo en lugares nuevos.
Lo siento, pero no vas a resolver esto desde el principio al establecer reglas generales. Necesita ver un código en particular, y decidir por sí mismo si ayuda o no a la legibilidad para especificar tipos en todo el lugar: ¿es mejor para su código decir, "el tipo de esto es X", o es mejor para su código para decir, "el tipo de esto es irrelevante para entender esta parte del código: el compilador necesita saber y probablemente podamos resolverlo pero no necesitamos decirlo aquí".
Dado que la "legibilidad" no se define objetivamente [*], y además varía según el lector, usted tiene la responsabilidad como autor / editor de un fragmento de código que una guía de estilo no puede satisfacer del todo. Incluso en la medida en que una guía de estilo especifique normas, diferentes personas preferirán normas diferentes y tenderán a considerar que todo lo que no se conoce es "menos legible". Por lo tanto, la legibilidad de una regla de estilo propuesta en particular a menudo solo se puede juzgar en el contexto de las otras reglas de estilo vigentes.
Todos sus escenarios (incluso el primero) encontrarán uso para el estilo de codificación de alguien. Personalmente, considero que el segundo es el caso de uso más convincente, pero aun así anticipo que dependerá de sus herramientas de documentación. No es muy útil ver documentado que el tipo de devolución de una plantilla de función es auto
, mientras que verlo documentado como decltype(t+u)
crea una interfaz publicada en la que puede (con suerte) confiar.
[*] Ocasionalmente, alguien intenta hacer algunas mediciones objetivas. En la medida en que a alguien se le ocurre algún resultado estadísticamente significativo y aplicable en general, los programadores en pleno trabajo lo ignoran por completo, a favor de los instintos del autor de lo que es "legible".
Con GCC 4.8.0 lanzado, tenemos un compilador que admite la deducción automática del tipo de retorno, parte de C ++ 14. Con -std=c++1y
, puedo hacer esto:
auto foo() { //deduced to be int
return 5;
}
Mi pregunta es: ¿Cuándo debería usar esta función? ¿Cuándo es necesario y cuándo hace que el código sea más limpio?
escenario 1
El primer escenario en el que puedo pensar es siempre que sea posible. Cada función que se puede escribir de esta manera debe ser. El problema con esto es que no siempre hace que el código sea más legible.
Escenario 2
El siguiente escenario es evitar tipos de retorno más complejos. Como un ejemplo muy claro:
template<typename T, typename U>
auto add(T t, U u) { //almost deduced as decltype(t + u): decltype(auto) would
return t + u;
}
No creo que eso sea realmente un problema, aunque creo que tener el tipo de devolución explícitamente depende de los parámetros podría ser más claro en algunos casos.
Escenario 3
A continuación, para evitar la redundancia:
auto foo() {
std::vector<std::map<std::pair<int, double>, int>> ret;
//fill ret in with stuff
return ret;
}
En C ++ 11, a veces solo podemos return {5, 6, 7};
en lugar de un vector, pero eso no siempre funciona y necesitamos especificar el tipo tanto en el encabezado de la función como en el cuerpo de la función. Esto es puramente redundante, y la deducción automática del tipo de devolución nos salva de esa redundancia.
Escenario 4
Finalmente, puede usarse en lugar de funciones muy simples:
auto position() {
return pos_;
}
auto area() {
return length_ * width_;
}
A veces, sin embargo, podemos ver la función, queriendo saber el tipo exacto, y si no se proporciona allí, tenemos que ir a otro punto en el código, como donde se declara pos_
.
Conclusión
Con esos escenarios establecidos, ¿cuál de ellos realmente demuestra ser una situación donde esta característica es útil para hacer que el código sea más limpio? ¿Qué hay de los escenarios que he olvidado mencionar aquí? ¿Qué precauciones debo tomar antes de usar esta función para que no me muerda más tarde? ¿Hay algo nuevo que esta característica trae a la mesa que no es posible sin él?
Tenga en cuenta que las preguntas múltiples están destinadas a ser una ayuda para encontrar perspectivas desde las cuales responder a esto.
Considere un entorno de producción real: muchas funciones y pruebas unitarias interdependientes en el tipo de devolución de foo()
. Ahora supongamos que el tipo de devolución debe cambiar por cualquier razón.
Si el tipo de devolución es auto
todas partes, y las personas que llaman a foo()
y las funciones relacionadas usan auto
cuando obtienen el valor devuelto, los cambios que deben hacerse son mínimos. Si no, esto podría significar horas de trabajo extremadamente tedioso y propenso a errores.
Como un ejemplo del mundo real, me pidieron que cambiara un módulo de usar punteros sin procesar en todas partes a punteros inteligentes. Reparar las pruebas unitarias fue más doloroso que el código real.
Si bien hay otras maneras en que esto se puede manejar, el uso de los tipos de devolución auto
parece ser una buena opción.
En términos generales, el tipo de devolución de función es de gran ayuda para documentar una función. El usuario sabrá lo que se espera. Sin embargo, hay un caso en el que creo que sería bueno descartar ese tipo de devolución para evitar la redundancia. Aquí hay un ejemplo:
template<typename F, typename Tuple, int... I>
auto
apply_(F&& f, Tuple&& args, int_seq<I...>) ->
decltype(std::forward<F>(f)(std::get<I>(std::forward<Tuple>(args))...))
{
return std::forward<F>(f)(std::get<I>(std::forward<Tuple>(args))...);
}
template<typename F, typename Tuple,
typename Indices = make_int_seq<std::tuple_size<Tuple>::value>>
auto
apply(F&& f, Tuple&& args) ->
decltype(apply_(std::forward<F>(f), std::forward<Tuple>(args), Indices()))
{
return apply_(std::forward<F>(f), std::forward<Tuple>(args), Indices());
}
Este ejemplo está tomado del documento oficial del comité N3493 . El objetivo de la función apply
es reenviar los elementos de una std::tuple
a una función y devolver el resultado. El int_seq
y make_int_seq
son solo parte de la implementación, y probablemente solo confundan a cualquier usuario que intente comprender lo que hace.
Como puede ver, el tipo de devolución no es más que un decltype
de la expresión devuelta. Además, apply_
no destinado a ser visto por los usuarios, no estoy seguro de la utilidad de documentar su tipo de devolución cuando es más o menos el mismo que el de apply
. Creo que, en este caso particular, descartar el tipo de devolución hace que la función sea más legible. Tenga en cuenta que este tipo de devolución realmente se ha eliminado y reemplazado por decltype(auto)
en la propuesta para agregar apply
al estándar, N3915 (también tenga en cuenta que mi respuesta original es anterior a este documento):
template <typename F, typename Tuple, size_t... I>
decltype(auto) apply_impl(F&& f, Tuple&& t, index_sequence<I...>) {
return forward<F>(f)(get<I>(forward<Tuple>(t))...);
}
template <typename F, typename Tuple>
decltype(auto) apply(F&& f, Tuple&& t) {
using Indices = make_index_sequence<tuple_size<decay_t<Tuple>>::value>;
return apply_impl(forward<F>(f), forward<Tuple>(t), Indices{});
}
Sin embargo, la mayoría de las veces, es mejor mantener ese tipo de devolución. En el caso particular que describí anteriormente, el tipo de devolución es bastante ilegible y un usuario potencial no obtendrá nada al conocerlo. Una buena documentación con ejemplos será mucho más útil.
Otra cosa que no se ha mencionado aún: mientras que declype(t+u)
permite usar la expresión SFINAE , decltype(auto)
no (aunque hay una propuesta para cambiar este comportamiento). Tomemos por ejemplo una función foobar
que llamará a la función foo
miembro de un tipo si existe o llamará a la función miembro de la bar
del tipo, si existe, y supondrá que una clase siempre tiene foo
o bar
exactos pero ninguno de los dos a la vez:
struct X
{
void foo() const { std::cout << "foo/n"; }
};
struct Y
{
void bar() const { std::cout << "bar/n"; }
};
template<typename C>
auto foobar(const C& c) -> decltype(c.foo())
{
return c.foo();
}
template<typename C>
auto foobar(const C& c) -> decltype(c.bar())
{
return c.bar();
}
Llamar a foobar
en una instancia de X
mostrará foo
mientras que llamar a foobar
en una instancia de Y
mostrará la bar
. Si utiliza la deducción de tipo de devolución automática en su lugar (con o sin tipo decltype(auto)
), no obtendrá la expresión SFINAE y llamar a foobar
en una instancia de X
o Y
desencadenará un error en tiempo de compilación.
No tiene nada que ver con la simplicidad de la función (como se suponía que era un duplicado de esta pregunta).
O el tipo de devolución es fijo (no use auto
) o depende de manera compleja de un parámetro de plantilla (use auto
en la mayoría de los casos, emparejado con decltype
cuando haya múltiples puntos de retorno).
Nunca es necesario En cuanto a cuándo deberías, obtendrás muchas respuestas diferentes sobre eso. Diría que no, hasta que sea una parte aceptada del estándar y que la mayoría de los principales compiladores la respalden de la misma manera.
Más allá de eso, va a ser un argumento religioso. Yo personalmente diría que nunca poner el tipo de devolución real hace que el código sea más claro, es mucho más fácil de mantener (puedo ver la firma de una función y saber qué devuelve en realidad teniendo que leer el código), y elimina la posibilidad de que crees que debería devolver un tipo y el compilador piensa que otro causa problemas (como ha sucedido con cada lenguaje de scripting que he usado alguna vez). Creo que el auto fue un error gigantesco y causará una cantidad de órdenes de magnitud mayor que la ayuda. Otros dirán que debe usarlo todo el tiempo, ya que se ajusta a su filosofía de programación. En cualquier caso, esto está fuera del alcance de este sitio.
Quiero brindar un ejemplo en el que el tipo de devolución automática sea perfecto:
Imagine que desea crear un alias corto para una llamada de función larga posterior. Con el auto, no es necesario que se ocupe del tipo de devolución original (tal vez cambie en el futuro) y el usuario puede hacer clic en la función original para obtener el tipo de devolución real:
inline auto CreateEntity() { return GetContext()->GetEntityManager()->CreateEntity(); }
PD: Depende de this pregunta.