¿El auto hace que el código de C ++ sea más difícil de entender?

117

Vi una conferencia de Herb Sutter en la que alienta a todos los programadores de C ++ a utilizar auto .

Hace algún tiempo tuve que leer el código C # donde se usaba ampliamente var y el código era muy difícil de entender: cada vez que se usaba var tuve que verificar el tipo de retorno del lado derecho. A veces, más de una vez, porque olvidé el tipo de la variable después de un tiempo!

Sé que el compilador sabe el tipo y no tengo que escribirlo, pero está ampliamente aceptado que deberíamos escribir código para los programadores, no para los compiladores.

También sé que es más fácil escribir:

auto x = GetX();

Que:

someWeirdTemplate<someOtherVeryLongNameType, ...>::someOtherLongType x = GetX();

Pero esto se escribe solo una vez y el tipo de retorno GetX() se verifica muchas veces para comprender qué tipo x tiene.

Esto me hizo preguntarme: ¿ auto hace que el código C ++ sea más difícil de entender?

    
pregunta Felics 20.12.2012 - 15:01
fuente

14 respuestas

91

Respuesta corta: más completamente, mi opinión actual sobre auto es que debería usar auto por defecto a menos que desee explícitamente una conversión. (Un poco más preciso, "... a menos que desee comprometerse explícitamente con un tipo, lo que casi siempre es porque quiere una conversión").

Respuesta más larga y razonamiento:

Escriba un tipo explícito (en lugar de auto ) solo cuando realmente quiera comprometerse explícitamente con un tipo, lo que casi siempre significa que desea obtener explícitamente una conversión a ese tipo. Desde lo alto de mi cabeza, recuerdo dos casos principales:

  • (Común) La sorpresa initializer_list que auto x = { 1 }; deduce initializer_list . Si no quiere initializer_list , diga el tipo, es decir, solicite explícitamente una conversión.
  • (Raro) El caso de las plantillas de expresión, como por ejemplo que auto x = matrix1 * matrix 2 + matrix3; captura un tipo de ayudante o proxy que no debe ser visible para el programador. En muchos casos, es correcto y benigno capturar ese tipo, pero a veces, si realmente desea que se colapse y haga el cálculo, diga el tipo, es decir, una vez más, solicite explícitamente una conversión.

De forma predeterminada, use auto de forma predeterminada, ya que el uso de auto evita las trampas y hace que su código sea más correcto, más fácil de mantener, robusto y más eficiente. Aproximadamente en orden de lo más a lo menos importante, en el espíritu de "escribir para mayor claridad y corrección primero":

  • Corrección: el uso de auto garantiza que obtendrá el tipo correcto. Como dice el dicho, si se repite (diga el tipo de forma redundante), puede y mentirá (hacerlo mal). Este es un ejemplo habitual: void f( const vector<int>& v ) { for( /*…* : en este punto, si escribe el tipo del iterador explícitamente, debe recordar escribir const_iterator (¿lo hizo?), Mientras que auto simplemente lo hace correctamente.
  • Capacidad de mantenimiento y robustez: El uso de auto hace que su código sea más sólido ante el cambio, porque cuando cambia el tipo de expresión, auto continuará resolviendo el tipo correcto. Si, por el contrario, se compromete con un tipo explícito, el cambio del tipo de expresión inyectará conversiones silenciosas cuando el nuevo tipo se convierta en el tipo anterior, o la construcción innecesaria se rompa cuando el nuevo tipo aún funciona, como el tipo anterior pero no se convierte al antiguo. escriba (por ejemplo, cuando cambia un map a un unordered_map , lo que siempre está bien si no confía en el pedido, utilizando auto para sus iteradores cambiará sin problemas de map<>::iterator a unordered_map<>::iterator , pero usar map<>::iterator en todas partes significa explícitamente que perderá su valioso tiempo en una onda de corrección de código mecánico, a menos que un pasante pase por allí y pueda evitar el trabajo aburrido en ellos).
  • Rendimiento: Como auto garantiza que no ocurrirá una conversión implícita, garantiza un mejor rendimiento de forma predeterminada. Si, en cambio, dice el tipo y requiere una conversión, a menudo obtendrá una conversión independientemente de si la esperaba o no.
  • Usabilidad: Usar auto es su única buena opción para los tipos difíciles de deletrear e inescrutables, como lambdas y ayudantes de plantillas, sin tener que recurrir a expresiones repetitivas de decltype o direccionamientos indirectos menos eficientes como std::function .
  • Conveniencia: Y, sí, auto es menos tipeo. Menciono que lo último está completo porque es una razón común para que te guste, pero no es la razón más grande para usarlo.

Por lo tanto: prefiero decir auto por defecto. Ofrece tanta simplicidad, rendimiento y claridad que solo te estás lastimando a ti mismo (y a los futuros mantenedores de tu código) si no lo haces. Solo confirme un tipo explícito cuando realmente lo dice en serio, lo que casi siempre significa que desea una conversión explícita.

Sí, hay (ahora) un GotW sobre esto.

    
respondido por el Herb Sutter 25.12.2012 - 21:06
fuente
110

Es una situación caso por caso.

A veces hace que el código sea más difícil de entender, a veces no. Tomemos, por ejemplo:

void foo(const std::map<int, std::string>& x)
{
   for ( auto it = x.begin() ; it != x.end() ; it++ )
   { 
       //....
   }
}

es definitivamente fácil de entender y definitivamente más fácil de escribir que la declaración del iterador real.

He estado usando C ++ por un tiempo, pero puedo garantizar que obtendré un error de compilación en mi primer intento porque olvidé el const_iterator e inicialmente iría por el iterator ... :)

Lo usaría para casos como este, pero no en el que realmente confunde el tipo (como tu situación), pero esto es puramente subjetivo.

    
respondido por el Luchian Grigore 20.12.2012 - 15:03
fuente
94

Míralo de otra manera. Escribes:

std::cout << (foo() + bar()) << "\n";

o:

// it is important to know the types of these values
int f = foo();
size_t b = bar();
size_t total = f + b;

std::cout << total << "\n";

A veces no ayuda a deletrear explícitamente el tipo.

La decisión de si necesita mencionar el tipo no es la misma que si desea dividir el código en varias declaraciones mediante la definición de variables intermedias. En C ++ 03 los dos estaban vinculados, puede pensar en auto como una forma de separarlos.

Algunas veces, hacer que los tipos sean explícitos puede ser útil:

// seems legit    
if (foo() < bar()) { ... }

vs.

// ah, there's something tricky going on here, a mixed comparison
if ((unsigned int)foo() < bar()) { ... }

En los casos en los que declara una variable, el uso de auto permite que el tipo quede sin hablar tal como aparece en muchas expresiones. Probablemente deberías intentar decidir por ti mismo cuando eso ayuda a la legibilidad y cuando dificulta.

Puede argumentar que, para empezar, mezclar tipos firmados y no firmados es un error (de hecho, algunos argumentan además que no se deben usar tipos sin firmar). La razón es posiblemente un error porque hace que los tipos de los operandos sean de vital importancia debido al comportamiento diferente. Si es malo tener que saber los tipos de sus valores, entonces probablemente tampoco sea malo no tener que conocerlos. Entonces, siempre que el código no sea confuso por otras razones, eso hace que auto OK, ¿verdad? ;-)

Particularmente cuando se escribe un código genérico, hay casos en que el tipo real de una variable no debería ser importante, lo que importa es que satisfaga la interfaz requerida. Así que auto proporciona un nivel de abstracción en el que ignora el tipo (pero, por supuesto, el compilador no lo sabe). Trabajar a un nivel adecuado de abstracción puede ayudar bastante a la legibilidad, trabajar en el nivel "incorrecto" hace que leer el código sea un problema.

    
respondido por el Steve Jessop 20.12.2012 - 15:08
fuente
26

OMI, estás viendo esto bastante al revés.

No es una cuestión de auto que conduzca a un código que sea ilegible o incluso menos legible. Es una cuestión de (esperar que) tener un tipo explícito para el valor de retorno compensará el hecho de que (aparentemente) no está claro qué tipo sería devuelto por alguna función en particular.

Al menos en mi opinión, si tiene una función cuyo tipo de retorno no es inmediatamente obvio, ese es su problema allí mismo. Lo que hace la función debería ser obvio por su nombre, y el tipo de valor de retorno debería ser obvio por lo que hace. Si no es así, esa es la verdadera fuente del problema.

Si hay un problema aquí, no es con auto . Es con el resto del código, y es muy probable que el tipo explícito sea solo una curita para evitar que vea y / o solucione el problema principal. Una vez que haya solucionado ese problema real, la legibilidad del código usando auto generalmente estará bien.

Supongo que, para ser justos, debo añadir: he tratado algunos casos en los que tales cosas no fueron tan obvias como a usted le gustaría, y solucionar el problema también fue bastante insostenible. Solo por un ejemplo, hice una consultoría para una compañía hace un par de años que anteriormente se había fusionado con otra compañía. Terminaron con una base de código que fue más "agrupada" de lo que realmente se fusionó. Los programas constitutivos habían comenzado a usar bibliotecas diferentes (pero bastante similares) para propósitos similares, y aunque estaban trabajando para fusionar las cosas de manera más limpia, todavía lo hacían. En un buen número de casos, la única manera de adivinar qué tipo sería devuelto por una función dada era saber dónde se había originado esa función.

Incluso en ese caso, puedes ayudar a aclarar algunas cosas. En ese caso, todo el código comenzó en el espacio de nombres global. El simple hecho de mover una cantidad justa a algunos espacios de nombres eliminó los conflictos de nombres y facilitó bastante el seguimiento de tipos.

    
respondido por el Jerry Coffin 21.12.2012 - 06:51
fuente
14

Sí, hace que sea más fácil saber el tipo de su variable si no usa auto . La pregunta es: ¿ necesita saber el tipo de su variable para leer el código? A veces la respuesta será sí, a veces no. Por ejemplo, al obtener un iterador de un std::vector<int> , ¿necesita saber que es un std::vector<int>::iterator o sería suficiente auto iterator = ...; ? Todo lo que cualquiera querría hacer con un iterador está dado por el hecho de que es un iterador, simplemente no importa cuál sea el tipo específico.

Use auto en aquellas situaciones en las que no haga que su código sea más difícil de leer.

    
respondido por el Joseph Mansfield 20.12.2012 - 15:05
fuente
14

Hay varias razones por las que no me gusta el auto para uso general:

  1. Puedes refactorizar el código sin modificarlo. Sí, esta es una de las cosas que a menudo se enumeran como un beneficio del uso del auto. Simplemente cambie el tipo de retorno de una función, y si todo el código que lo llama se usa automáticamente, ¡no se requiere ningún esfuerzo adicional! Presiona compilar, genera - 0 advertencias, 0 errores - y simplemente sigue adelante y verifica tu código sin tener que lidiar con el desorden de revisar y potencialmente modificar los 80 lugares donde se usa la función.

Pero espera, ¿es realmente una buena idea? ¿Qué pasa si el tipo importaba en media docena de esos casos de uso, y ahora ese código realmente se comporta de manera diferente? Esto también puede romper implícitamente la encapsulación, al modificar no solo los valores de entrada, sino también el comportamiento de la implementación privada de otras clases que llaman a la función.

1a. Soy un creyente en el concepto de "código de auto-documentación". El razonamiento detrás del código de autodocumentación es que los comentarios tienden a quedar desactualizados, ya no reflejan lo que el código está haciendo, mientras que el código en sí, si se escribe de manera explícita, se explica por sí mismo, siempre se mantiene actualizado. en su intención, y no te dejará confundido con comentarios pasados. Sin embargo, si los tipos se pueden cambiar sin necesidad de modificar el propio código, entonces el código / las variables pueden volverse obsoletos. Por ejemplo:

auto bThreadOK = CheckThreadHealth ();

Excepto que el problema es que CheckThreadHealth () en algún punto fue refactorizado para devolver un valor de enumeración que indica el estado de error, en su caso, en lugar de un bool. Pero la persona que hizo ese cambio no pudo inspeccionar esta línea de código en particular, y el compilador no sirvió de nada porque se compiló sin advertencias ni errores.

  1. Nunca se puede saber cuáles son los tipos reales. Esto también suele aparecer como un "beneficio" primario del auto. ¿Por qué aprender lo que una función te está dando cuando puedes decir simplemente "A quién le importa? ¡Se compila!"

Incluso funciona un poco, probablemente. Digo que tipo de trabajo, porque aunque está haciendo una copia de una estructura de 500 bytes para cada iteración de bucle, por lo que puede inspeccionar un solo valor, el código sigue siendo completamente funcional. Así que incluso sus pruebas de unidad no lo ayudan a darse cuenta de que el código incorrecto se esconde detrás de ese auto simple e inocente. La mayoría de las otras personas que escanean el archivo tampoco lo notarán a primera vista.

Esto también puede empeorar si no sabe cuál es el tipo, pero elige un nombre de variable que haga una suposición errónea acerca de qué es, logrando el mismo resultado que en 1a, pero desde el principio comenzando en lugar de post-refactor.

  1. Escribir el código cuando lo escribes inicialmente no es la parte más lenta de la programación. Sí, el auto hace que escribir un código más rápido inicialmente. Como descargo de responsabilidad, escribo > 100 WPM, así que tal vez no me moleste tanto como a los demás. Pero si todo lo que tenía que hacer era escribir un código nuevo todo el día, sería un campista feliz. La parte de la programación que más tiempo consume es diagnosticar errores en el código de casos difíciles de reproducir en el código, a menudo como resultado de problemas sutiles no obvios, como el tipo de uso excesivo de automóviles que probablemente se presente (referencia vs. copia, firmado vs. no firmado, flotante vs. int, bool vs. puntero, etc.).

Me parece obvio que auto se introdujo principalmente como una solución para la sintaxis terrible con los tipos de plantillas de biblioteca estándar. En lugar de intentar corregir la sintaxis de la plantilla con la que ya están familiarizados, lo que también puede ser casi imposible de hacer debido a todo el código existente que podría romper, agregue una palabra clave que básicamente oculte el problema. Esencialmente, lo que podríamos llamar un "hack".

En realidad no tengo ningún desacuerdo con el uso de auto con contenedores de biblioteca estándar. Obviamente, es para lo que se creó la palabra clave, y no es probable que las funciones de la biblioteca estándar cambien fundamentalmente en su propósito (o tipo de letra), haciendo que el auto sea relativamente seguro de usar. Pero sería muy cauteloso al usarlo con su propio código e interfaces que pueden ser mucho más volátiles y potencialmente estar sujetos a cambios más fundamentales.

Otra aplicación útil de auto que mejora la capacidad del lenguaje es la creación de temporarios en macros de tipo agnóstico. Esto es algo que realmente no podías hacer antes, pero puedes hacerlo ahora.

    
respondido por el Tim W 25.07.2015 - 03:09
fuente
12

Personalmente uso auto solo cuando es absolutamente obvio para el programador lo que es.

Ejemplo 1

std::map <KeyClass, ValueClass> m;
// ...
auto I = m.find (something); // OK, find returns an iterator, everyone knows that

Ejemplo 2

MyClass myObj;
auto ret = myObj.FindRecord (something)// NOT OK, everyone needs to go and check what FindRecord returns
    
respondido por el aleguna 20.12.2012 - 15:09
fuente
10

Esta pregunta solicita una opinión, que variará de un programador a otro, pero yo diría que no. De hecho, en muchos casos todo lo contrario, auto puede ayudar a hacer que el código sea más fácil de entender al permitir que el programador se centre en la lógica en lugar de las minucias.

Esto es especialmente cierto en el caso de tipos de plantillas complejas. Aquí hay un simplificado & Ejemplo artificial. ¿Qué es más fácil de entender?

for( std::map<std::pair<Foo,Bar>, std::pair<Baz, Bot>, std::less<BazBot>>::const_iterator it = things_.begin(); it != things_.end(); ++it )

.. o ...

for( auto it = things_.begin(); it != things_.end(); ++it )

Algunos dirían que el segundo es más fácil de entender, otros pueden decir el primero. Sin embargo, otros podrían decir que un uso gratuito de auto puede contribuir a una disminución de los programadores que lo usan, pero esa es otra historia.

    
respondido por el John Dibling 20.12.2012 - 15:06
fuente
8

Muchas buenas respuestas hasta ahora, pero para centrarme en la pregunta original, creo que Herb va demasiado lejos en su consejo para usar auto liberalmente. Su ejemplo es un caso en el que usar auto obviamente daña la legibilidad. Algunas personas insisten en que no es un problema con los IDE modernos donde se puede desplazar sobre una variable y ver el tipo, pero no estoy de acuerdo: incluso las personas que siempre usan un IDE a veces necesitan mirar fragmentos de código de forma aislada (piense en las revisiones de código , por ejemplo) y un IDE no ayudará.

Línea inferior: use auto cuando ayude: es decir, iteradores para bucles. No lo use cuando haga que el lector tenga dificultades para averiguar el tipo.

    
respondido por el Nemanja Trifunovic 21.12.2012 - 14:52
fuente
5

Estoy bastante sorprendido de que nadie haya señalado todavía que el auto ayuda si no hay un tipo claro. En este caso, puede solucionar este problema utilizando #define o typedef en una plantilla para encontrar el tipo utilizable real (y esto a veces no es trivial), o simplemente usa auto.

Supongamos que tienes una función que devuelve algo con el tipo específico de plataforma:

#ifdef PLATFROM1
__int256 getStuff();
#else //PLATFORM2
__int128 getStuff();
#endif

¿Prefieres el uso de brujas?

#ifdef PLATFORM1
__int256 stuff = getStuff();
#else
__int128 stuff = getStuff();
#endif

o simplemente

auto stuff = getStuff();

Claro, puedes escribir

#define StuffType (...)

también en algún lugar, pero lo hace

StuffType stuff = getStuff();
¿

en realidad dices algo más sobre el tipo de x? Dice que es lo que se devuelve desde allí, pero es exactamente lo que es auto. Esto es simplemente redundante: "cosas" se escriben 3 veces aquí, en mi opinión esto lo hace menos legible que la versión "automática".

    
respondido por el Lrdx 27.12.2012 - 12:38
fuente
3

La legibilidad es subjetiva; Tendrá que mirar la situación y decidir qué es lo mejor.

Como señalaste, sin auto, las declaraciones largas pueden producir mucho desorden. Pero como también señaló, las declaraciones cortas pueden eliminar información de tipo que puede ser valiosa.

Además de esto, también agregaría esto: asegúrate de que estás viendo la legibilidad y no la capacidad de escritura. El código que es fácil de escribir generalmente no es fácil de leer y viceversa. Por ejemplo, si estuviera escribiendo, preferiría auto. Si estuviera leyendo, tal vez las declaraciones más largas.

Entonces hay consistencia; ¿Qué tan importante es eso para ti? ¿Querría auto en algunas partes y declaraciones explícitas en otras, o un método consistente en todas partes?

    
respondido por el user1161318 20.12.2012 - 15:02
fuente
2

Tomaré el punto de un código menos legible como una ventaja, y alentaré al programador a usarlo cada vez más. ¿Por qué? Claramente, si el código que usa auto es difícil de leer, también será difícil escribirlo. El programador está obligado a usar el nombre de variable significativo para mejorar su trabajo.
Tal vez al principio el programador no escriba los nombres de las variables significativas. Pero eventualmente, mientras arregla los errores, o en la revisión del código, cuando él / ella tiene que explicar el código a otros, o en un futuro no muy cercano, explica el código a las personas de mantenimiento, el programador se dará cuenta del error y lo usará El nombre de la variable significativa en el futuro.

    
respondido por el Manoj R 21.12.2012 - 07:44
fuente
2

Tengo dos pautas:

  • Si el tipo de la variable es obvio, tedioso para escribir o difícil de escribir determinar el uso automático.

    auto range = 10.0f; // Obvious
    
    for (auto i = collection.cbegin(); i != cbegin(); ++i) // Tedious if collection type
    // is really long
    
    template <typename T> ... T t; auto result = t.get(); // Hard to determine as get()
    // might return various stuff
    
  • Si necesita una conversión específica o el tipo de resultado no es obvio y puede causar confusión.

    class B : A {}; A* foo = new B(); // 'Convert'
    
    class Factory { public: int foo(); float bar(); }; int f = foo(); // Not obvious
    
respondido por el Red XIII 15.01.2013 - 10:33
fuente
1

Sí. Disminuye la verbosidad, pero el malentendido común es que la verbosidad disminuye la legibilidad. Esto solo es cierto si considera que la legibilidad es estética en lugar de su capacidad real para interpretar el código, lo que no aumenta con el uso del auto. En el ejemplo más citado, los iteradores vectoriales, puede aparecer en la superficie que el uso automático aumenta la legibilidad de su código. Por otro lado, no siempre sabe qué le dará la palabra clave automática. Tienes que seguir el mismo camino lógico que el compilador para hacer esa reconstrucción interna, y muchas veces, en particular con los iteradores, harás las suposiciones erróneas.

Al final del día, 'auto' sacrifica la legibilidad del código y la claridad, por la 'limpieza' sintáctica y estética (que solo es necesaria porque los iteradores tienen una sintaxis complicada innecesariamente) y la capacidad de escribir 10 caracteres menos en un determinado línea. No vale la pena el riesgo, o el esfuerzo involucrado a largo plazo.

    
respondido por el metamorphosis 30.10.2014 - 02:50
fuente

Lea otras preguntas en las etiquetas