¿Cuándo debo usar la deducción automática del tipo de devolución C ++ 14?

144

Con el lanzamiento de GCC 4.8.0, tenemos un compilador que admite la deducción automática del tipo de devolución, parte de C ++ 14. Con -std=c++1y, puedo hacer esto:

auto foo() { //deduced to be int
    return 5;
}

Mi pregunta es: ¿Cuándo debo 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. Todas las funciones que se pueden escribir de esta manera deberían serlo. El problema con esto es que no siempre puede hacer 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 ligero:

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 realmente sea un problema, aunque supongo que tener el tipo de retorno depende explícitamente 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 podemos simplemente return {5, 6, 7};reemplazar un vector, pero eso no siempre funciona y necesitamos especificar el tipo en el encabezado y 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, se puede usar en lugar de funciones muy simples:

auto position() {
    return pos_;
}

auto area() {
    return length_ * width_;
}

Sin embargo, a veces, podemos mirar 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 pos_se declara.

Conclusión

Con esos escenarios establecidos, ¿cuál de ellos demuestra ser una situación en la que esta característica es útil para hacer que el código sea más limpio? ¿Qué pasa con 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 sea posible sin ella?

Tenga en cuenta que las múltiples preguntas están destinadas a ayudar a encontrar perspectivas desde las cuales responder esto.

Chris
fuente
18
Maravillosa pregunta! Mientras pregunta qué escenarios hacen que el código sea "mejor", también me pregunto qué escenarios lo empeorarán .
Drew Dormann el
2
@DrewDormann, eso es lo que me pregunto también. Me gusta hacer uso de las nuevas funciones, pero saber cuándo usarlas y cuándo no es muy importante. Hay un período de tiempo cuando surgen nuevas características que tomamos para resolver esto, así que hagámoslo ahora para que estemos listos para cuando llegue oficialmente :)
Chris
2
@NicolBolas, tal vez, pero el hecho de que esté en una versión real de un compilador ahora sería suficiente para que las personas comiencen a usarlo en código personal (definitivamente, en este momento, debe mantenerse alejado de los proyectos). Soy una de esas personas a las que les gusta usar las funciones más nuevas posibles en mi propio código, y aunque no sé qué tan bien va la propuesta con el comité, creo que el hecho de que sea la primera incluida en esta nueva opción dice alguna cosa. Puede que sea mejor dejarlo para más tarde, o (no sé qué tan bien funcionaría) revivido cuando sepamos con certeza que llegará.
Chris
1
@NicolBolas, si ayuda, se ha adoptado ahora: p
chris
1
Las respuestas actuales no parecen mencionar que reemplazar ->decltype(t+u)con deducción automática mata a SFINAE.
Marc Glisse el

Respuestas:

62

C ++ 11 plantea preguntas similares: cuándo usar la deducción de tipo de retorno en lambdas y cuándo usar autovariables.

La respuesta tradicional a la pregunta en C y C ++ 03 ha sido "a través de los límites de las declaraciones, hacemos explícitos los tipos, dentro de las expresiones generalmente están implícitas, pero podemos hacerlas explícitas con conversiones". C ++ 11 y C ++ 1y presentan herramientas de deducción de tipos para que pueda omitir el tipo en nuevos lugares.

Lo sentimos, pero no vas a resolver esto por adelantado haciendo reglas generales. Debe mirar 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 que su código diga "el tipo de esta cosa es X", o es mejor para su código para decir, "el tipo de esta cosa es irrelevante para comprender esta parte del código: el compilador necesita saber y probablemente podríamos resolverlo, pero no necesitamos decirlo aquí".

Dado que la "legibilidad" no está definida objetivamente [*], y además varía según el lector, usted tiene la responsabilidad como autor / editor de un código que no puede ser totalmente satisfecho por una guía de estilo. Incluso en la medida en que una guía de estilo especifique normas, diferentes personas preferirán normas diferentes y tenderán a encontrar algo desconocido para que sea "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í preveo que dependerá de sus herramientas de documentación. No es muy útil ver documentado que el tipo de retorno de una plantilla de función es auto, mientras que verlo documentado decltype(t+u)crea una interfaz publicada en la que puede (con suerte) confiar.

[*] Ocasionalmente, alguien intenta hacer algunas mediciones objetivas. En la pequeña medida en que alguien obtiene resultados estadísticamente significativos y aplicables en general, los programadores que trabajan trabajan los ignoran por completo, a favor de los instintos del autor de lo que es "legible".

Steve Jessop
fuente
1
Buen punto con las conexiones a lambdas (aunque esto permite cuerpos de función más complejos). Lo principal es que es una característica más nueva, y todavía estoy tratando de equilibrar los pros y los contras de cada caso de uso. Para hacer esto, es útil ver las razones detrás de por qué se usaría para qué, de modo que yo mismo pueda descubrir por qué prefiero lo que hago. Podría estar pensando demasiado, pero así es como soy.
Chris
1
@chris: como digo, creo que todo se reduce a lo mismo. ¿Es mejor que su código diga, "el tipo de esta cosa es X", o es mejor que su código diga, "el tipo de esta cosa es irrelevante, el compilador necesita saberlo y probablemente podríamos resolverlo pero no necesitamos decirlo ". Cuando escribimos 1.0 + 27Uestamos afirmando lo último, cuando escribimos (double)1.0 + (double)27Uestamos afirmando lo primero. La simplicidad de la función, el grado de duplicación y la evitación decltypepodrían contribuir a eso, pero ninguno será decisivo de manera confiable.
Steve Jessop
1
¿Es mejor que su código diga, "el tipo de esta cosa es X", o es mejor que su código diga, "el tipo de esta cosa es irrelevante, el compilador necesita saberlo y probablemente podríamos resolverlo pero no necesitamos decirlo ". - Esa oración está exactamente en la línea de lo que estoy buscando. Lo tendré en cuenta a medida que encuentre opciones de uso de esta función, y autoen general.
Chris
1
Me gustaría agregar que los IDE pueden aliviar el "problema de legibilidad". Tome Visual Studio, por ejemplo: si coloca el cursor sobre la autopalabra clave, se mostrará el tipo de retorno real.
andreee
1
@andreee: eso es cierto dentro de los límites. Si un tipo tiene muchos alias, entonces saber el tipo real no siempre es tan útil como cabría esperar. Por ejemplo, un tipo de iterador puede ser int*, pero lo que es realmente significativo, si es que lo hace, es que la razón int*es porque eso es lo que std::vector<int>::iterator_typeestá con sus opciones de compilación actuales.
Steve Jessop
30

En términos generales, el tipo de retorno 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 eliminar ese tipo de retorno 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 propósito de la función applyes reenviar los elementos de a std::tuplea una función y devolver el resultado. Los int_seqy make_int_seqson solo parte de la implementación, y probablemente solo confundirán a cualquier usuario que intente entender lo que hace.

Como puede ver, el tipo de retorno no es más que una decltypede las expresiones devueltas. Además, al apply_no estar destinado a ser visto por los usuarios, no estoy seguro de la utilidad de documentar su tipo de retorno cuando es más o menos igual al applyde uno. Creo que, en este caso particular, soltar el tipo de retorno hace que la función sea más legible. Tenga en cuenta que este mismo tipo de devolución se ha eliminado y reemplazado decltype(auto)en la propuesta para agregar applyal 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 retorno. En el caso particular que describí anteriormente, el tipo de retorno es bastante ilegible y un usuario potencial no obtendrá nada al saberlo. Una buena documentación con ejemplos será mucho más útil.


Otra cosa que aún no se ha mencionado: si bien declype(t+u)permite usar la expresión SFINAE , decltype(auto)no lo hace (aunque hay una propuesta para cambiar este comportamiento). Tomemos, por ejemplo, una foobarfunción que llamará a la foofunción miembro de un tipo si existe o llame a la barfunción miembro del tipo si existe, y suponga que una clase siempre tiene exactamente fooo barambas cosas 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 foobara una instancia de Xse mostrará foomientras se llama foobara una instancia de Yse mostrará bar. Si utiliza la deducción automática de tipo de retorno en su lugar (con o sin decltype(auto)), no obtendrá la expresión SFINAE ni invocará foobaruna instancia de ninguno de ellos Xo Yactivará un error en tiempo de compilación.

Morwenn
fuente
8

Nunca es necesario En cuanto a cuándo debería, obtendrá muchas respuestas diferentes al respecto. Yo diría que no hasta que sea una parte aceptada del estándar y bien respaldada por la mayoría de los principales compiladores de la misma manera.

Más allá de eso, va a ser un argumento religioso. Personalmente, diría que nunca poner el tipo de retorno real aclara el código, es mucho más fácil para el mantenimiento (puedo ver la firma de una función y saber lo que devuelve en lugar de tener que leer el código), y elimina la posibilidad de que cree que debería devolver un tipo y el compilador piensa que otro está causando problemas (como ha sucedido con cada lenguaje de script que he usado). Creo que el auto fue un gran error y causará mucho más dolor que 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.

Gabe Sechan
fuente
1
Estoy de acuerdo en que las personas con diferentes antecedentes tendrán diferentes opiniones sobre esto, pero espero que esto llegue a una conclusión de C ++. En (casi) todos los casos, es imperdonable abusar de las características del lenguaje para intentar convertirlo en otro, como usar #defines para convertir C ++ en VB. Las características generalmente tienen un buen consenso dentro de la mentalidad del lenguaje sobre qué es el uso apropiado y qué no, de acuerdo a lo que los programadores de ese lenguaje están acostumbrados. La misma característica puede estar presente en varios idiomas, pero cada uno tiene sus propias pautas sobre su uso.
Chris
2
Algunas características tienen un buen consenso. Muchos no lo hacen. Conozco a muchos programadores que piensan que la mayoría de Boost es basura que no debería usarse. También conozco a algunos que piensan que es lo mejor que le puede pasar a C ++. En cualquier caso, creo que hay algunas discusiones interesantes sobre él, pero en realidad es un ejemplo exacto de la opción cerrada como no constructiva en este sitio.
Gabe Sechan 01 de
2
No, estoy completamente en desacuerdo contigo, y creo que autoes pura bendición. Elimina mucha redundancia. Es simplemente un dolor repetir el tipo de retorno a veces. Si desea devolver una lambda, puede ser incluso imposible sin almacenar el resultado en a std::function, lo que puede generar algunos gastos generales.
Yongwei Wu
1
Hay al menos una situación en la que todo es completamente necesario. Suponga que necesita llamar a funciones, luego registrar los resultados antes de devolverlos, y las funciones no necesariamente tienen el mismo tipo de retorno. Si lo hace a través de una función de registro que toma las funciones y sus argumentos como parámetros, deberá declarar el tipo de retorno de la función de registro autopara que siempre coincida con el tipo de retorno de la función pasada.
Justin Time - Restablece a Mónica el
1
[Técnicamente, hay otra forma de hacer esto, pero utiliza la magia de la plantilla para hacer básicamente exactamente lo mismo, y todavía necesita que la función de registro sea autopara el tipo de retorno final.]
Justin Time - Reinstala Monica el
7

No tiene nada que ver con la simplicidad de la función (como supone un duplicado ahora eliminado de esta pregunta).

El tipo de retorno es fijo (no se usa auto) o depende de manera compleja de un parámetro de plantilla (se usa autoen la mayoría de los casos, emparejado decltypecuando hay múltiples puntos de retorno).

Ben Voigt
fuente
3

Considere un entorno de producción real: muchas funciones y pruebas unitarias, todas interdependientes en el tipo de retorno de foo(). Ahora suponga que el tipo de retorno necesita cambiar por cualquier razón.

Si el tipo de retorno está en autotodas partes, y las personas que llaman foo()y las funciones relacionadas se usan autoal obtener el valor devuelto, los cambios que deben realizarse son mínimos. Si no, esto podría significar horas de trabajo extremadamente tedioso y propenso a errores.

Como ejemplo del mundo real, me pidieron que cambiara un módulo del uso de punteros sin formato en todas partes a punteros inteligentes. Arreglar las pruebas unitarias fue más doloroso que el código real.

Si bien hay otras formas de manejar esto, el uso de autotipos de retorno parece ser una buena opción.

Jim Wood
fuente
1
En esos casos, ¿no sería mejor escribir decltype(foo())?
Oren S
Personalmente, creo que typedeflas declaraciones sy alias (es decir, la usingdeclaración de tipo) son mejores en estos casos, especialmente cuando tienen un alcance de clase.
MAChitgarha
3

Quiero proporcionar un ejemplo donde el tipo de retorno automático es perfecto:

Imagine que desea crear un alias corto para una llamada de función posterior larga. Con auto, no necesita cuidar el 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 esta pregunta.

emperador
fuente
2

Para el escenario 3, cambiaría el tipo de retorno de la firma de la función con la variable local que se devolverá. Sería más claro para los programadores del cliente que la función retorna. Me gusta esto:

Escenario 3 Para evitar la redundancia:

std::vector<std::map<std::pair<int, double>, int>> foo() {
    decltype(foo()) ret;
    return ret;
}

Sí, no tiene una palabra clave automática, pero el principal es el mismo para evitar la redundancia y brindar a los programadores que no tienen acceso a la fuente un momento más fácil.

Servir Laurijssen
fuente
2
En mi opinión, la mejor solución para esto es proporcionar un nombre específico de dominio para el concepto de lo que se pretende representar como a vector<map<pair<int,double>,int>y luego usarlo.
davidbak