1 2 3 4 5 6 7 8 12345678901234567890123456789012345678901234567890123456789012345678901234567890 /*****************************************************************************************************************/ Nota del traductor: El presente texto emprende el objetivo de ser fiel dentro de lo más ajustado de sus posibilidades en lo que respecta a la confección original de este texto que fue redactado en idioma inglés. Por consiguiente, el motivo de tal esfuerzo fue impulsado gracias a una acción altruista por parte del traductor que propone aquí el hecho de no quebrantar el código de honor por parte del lector; Debido a las posibilidades que tal formato de texto brinda, fácil sería plagiar este texto cambiando el nombre original del autor tanto como el del traductor. Dados los presentes argumentos, lo que a partir de este momento ocurra pesará sobre la consciencia del lector en función de su prestigio. Pedro Javier Etchegaray. /*****************************************************************************************************************/ [Nota: Esto es una copia de un reporte técnico el cual ha sido originalmente preparado usando Interleaf y distribuido con Postscript en modo Sólo-Texto. En el proceso de convertir este documento hacia formato txt; los títulos y subtítulos de las páginas, tanto como la confección agrupada en secciones del texto original, han sido perdidos, no obstante el quid de su contenido ha sido preservado sin modificaciones.] El caso contra C (the case against C) P.J. Moylan Department of Electrical and Computer Engineering The University of Newcastle N.S.W. 2308, Australia eepjm@wombat.newcastle.edu.au Fax: +61 49 60 1712 Abstracto El lenguaje de programación C ha estado en amplio uso desde la temprana década del 70', y es probablemente el lenguaje más extensamente usado por los profesionales de la ciencia informática. La meta de este escrito es argumentar sobre el hecho de que ya es tiempo de retirar el lenguaje C en favor de un lenguaje más moderno. La elección de un lenguaje de programación es algunas veces una consecuencia emocional, tal que no sería motivo para una discución racional. Sin embargo se espera mostrar aquí que existen buenas razones objetivas sobre el ¨Por qué C no es una buena opción a la hora de involucrarse con grandes proyectos en lo que respecta a la programación en si¨. Estas razones son expuestas principalmente en torno a la rentabilidad de programas tanto como para la productividad del mismo programador. Tópicos: Lenguajes de programación, C, C++ Introducción Esta nota fue escrita luego de haberme encontrado a mí mismo diciendo y escribiendo a diferentes personas, las mismas cosas una y otra vez. Antes de continuar repitiendo lo mismo, he pensado que debería sumarizar mis pensamientos en un sólo documento. Aunque el título de este texto suene frívolo, sin embargo es un documento serio. Estoy profundamente preocupado acerca del amplio uso del C para serios trabajos de programación. El lenguaje se ha difundido mucho mas allá de su propia e intensionada área de aplicación. Además, los entusiastas del C parecen estar ignorando plenamente los avances que se han hecho en cuanto al diseño de lenguajes durante los pasados 20 años. La indebida lealtad hacia el C es, en mi opinión, sólo un serio problema entre profesionales, tanto como lo es entre amateurs remitiéndose a lo básico. No es mi intensión debatir mediante esta nota las relativas cualidades procedimentales (ej. Pascal o C), funcionales (ej. Lisp) o las cualidades de lenguajes declarativos (ej. Prolog); Eso es un tema aparte. Mi intensión, más bien, es la de incitar a la gente que usa un lenguaje de procedimiento, para que den preferencia a lenguajes de alto-nivel. En lo que sigue, debería usar Modula-2 como ejemplo de un moderno lenguaje de programación. Esto es simple porque es un lenguaje acerca del cual puedo hablar de forma inteligente. No estoy sugiriendo que Modula-2 sea perfecto; pero al menos sirve para ilustrar que hay lenguajes que no tienen las fallas del lenguaje C. No considero al C++ del mismo modo; La cuestión del C++ será considerada en una futura sección. Por ahora, vale la pena señalar que casi todas las críticas del C serán listadas en esta nota, aplicando equitativamente bien estas críticas con respecto al 'C++' también. El lenguaje C++ es por supuesto una mejora del C, pero esto no soluciona algunos de los serios problemas que el C tiene en si. Algo de historia El primer compilador de C, en una PDP-11, apareció alrededor del año 1972. En el tiempo en que la PDP-11 fue relativamente una nueva máquina, y unos pocos lenguajes de programación estaban disponibles para esta; la opción fue esencialmente limitada al lenguaje ensamblador, BASIC, y Fortran IV. (Compiladores e intérpretes habian sido escritos para otros lenguajes, los cuales no tuvieron una gran distribución.) Dadas las obvias limitaciones que daban estos lenguajes para programación a nivel sistemas, hubo una clara necesidad de implantar un nuevo lenguaje. Esta fue además la era en la cual los diseñadores de software estaban empezando a aceptar que los sistemas operativos ya no necesitaban ser escritos en lenguaje ensamblador. La primera versión de Unix (1969-70) fue escrita en lenguaje ensamblador, pero subsecuentemente casi todos ellos fueron reescritos en C. Para hacer esto posible, de todos modos, era necesario tener un lenguaje que pudiera desviar algunos de los chequeos de seguridad que son construidos en la mayoría de los lenguajes de alto-nivel, y permitirle a uno que pueda hacer cosas que solo podrían ser hechas con lenguaje ensamblador o mediante el lenguaje máquina. Esto introdujo el concepto de lenguajes de nivel intermedio orientado al lenguaje máquina. C no fue el único lenguaje dentro de ese concepto, y naturalmente tampoco fue uno de los primeros. De hecho, una entera serie de lenguajes orientados al lenguaje máquina (machine-oriented) han aparecido al alrededor de ese tiempo. (Yo fui autor de uno de esos lenguajes, SGL, que fue usado para un número de proyectos dentro de nuestro departamento en los '70. Este fue retirado, como ya volviéndose algo fuera de moda, a comienzos de los '80.) Estos lenguajes tenian un fuerte parecido familiar entre ellos; Esto no quiere decir que los autores se hallan copiado entre si (En mi caso, SGL ha alcanzado un justo avance antes de que yo tome consciencia sobre la existencia del C), pero todos los lenguajes contemporáneos fueron influenciados por la misma fuente de ideas, las cuales eran comunes en propiedad de tal época. Por qué C se volvió popular La historia de C está inextricablemente relacionada con la historia de Unix. El mismo sistema operativo Unix está escrito en C, como la mayoría de los utilitarios que vienen con Unix; y a lo mejor de mi entender, cada distribución de Unix viene acompañadada de un compilador de C, mientras que nos es difícil conseguir compiladores para otros lenguajes bajo Unix. De esta manera, necesitamos observar las razones de la rápida difusión de Unix. Su costo y disponibilidad son las obvias razones. Unix fue distribuido virtualmente sin costo, y el código fuente estaba disponible para hacer que se instale fácilmente en otros sistemas. Un número de útiles programas eran disponibles dentro de Unix - escritos en C, por supuesto - y usualmente era mas fácil dejarlos en C, que traducirlos a otro lenguaje. Para un usuario de Unix que queria hacer algun tipo de programación, una competencia en lenguaje C era casi esencial. Desde entonces, C ha subsistido gracias a su extensa difusión por las mismas razones del porqué lo hizo Fortran: Un lenguaje que ha construido una gran base de usuarios, ocasionó un momento imparable. Cuando a la gente se le preguntaba ¨Por qué usas C?¨, las respuestas mas comunes eran (a) acceso a la disponibilidad de baratos compiladores; (b) una extensa librería de subrutinas y herramientas; (c) todos lo usan. La preparada disponibilidad de compiladores, librerías y herramientas de soporte es, por supuesto, una consecuencia directa de un largo número de usuarios. Y, por supuesto, cada generación de educadores enseña a sus alumnos el lenguaje C como su lenguaje favorito. La portabilidad también es dada como razón de la popularidad de C, pero en mi opinión esto es un pretexto para desviar la atención. El motivo de que C sea portable, vuelve casi imposible el hecho de convencer a los programadores para que se aferren a ese motivo. El compilador de C que yo uso puede generar mensajes de advertencia en lo que concierne a la portabilidad, pero esto no es un esfuerzo en lo absoluto por escribir un programa no-portable sobre el cual el compilador no genere mensajes de advertencia. Por qué C permanece siendo popular Con avances en la tecnología de los compiladores, la original motivación para el diseño de lenguajes de medio-nivel (a saber, la eficiencia del código objeto) en gran parte ha desaparecido. Junto con otros lenguajes orientados al lenguaje máquina que han hecho su aparición al mismo tiempo en que lo hizo el C son ahora considerados como obsoletos. Entonces por qué C ha sobrevivido? Hay por supuesto una creencia de que C tiene una atracción hacia el lado ¨macho¨ de los programadores, quienes disfrutan el reto de esforzarse con fallas obscuras, además de encontrar complicadas formas de hacer las cosas. La concisión del código C es también una popular característica. Los programadores de C parecen sentir habilidad para escribir una declaración como la siguiente: **p++^=q++=*r---s es un mayor argumento en favor del uso de C, con esto nos ahorraríamos el presionar teclas demás. Un cínico podría sugerir que tal ahorro compensaría la necesidad de escribir comentarios adicionales al programa, pero una mirada hacia típicos programas de C nos mostrará que los comentarios también son considerados como una pérdida de tiempo, incluso entre los tan-llamados programadores profesionales. Otro factor importante es que la característica inicial de un programa es percibida más rápido en C que en un lenguaje más estructurado. (No estoy de acuerdo con esta creencia, por lo tanto escribiré luego sobre este punto.) La percepción general es que se necesita un adelantado planeamiento para escribir en un lenguaje como Modula-2, mientras que en C uno puede sentarse y empezar a escribir inmediatamente, recibiendo así una mayor gratificación inmediata. Estas razones nos suenan familiares? Si, y son casi idénticas a los argumentos que rondaban hace años en favor del BASIC. Podría ser que la vigente cosecha de programadores son la misma gente que jugaron con computadoras de juguete cuando eran adolescentes? Hemos dicho que al tiempo de usar BASIC como primer lenguaje podría crear malos hábitos, los cuales nos serían muy difíciles de erradicar. Ahora estamos viendo la evidencia de eso. Avances en el diseño de lenguajes Sería una tarea para Gargantúa el hecho de volcar en forma documentada el origen de lo que conocemos hoy acerca del diseño de un lenguaje de programación, claro está que eso no será lo que hare aquí. (--Metáfora dilucidada por el traductor de este texto: Gargantúa es un gigante con gran capacidad para comer y beber, personaje principal y padre de Pantagruel en la novela escrita por Rabelais -Los horribles hechos y proezas espantables de Pantagruel(1535)--) Algunas de las buenas ideas aparecieron primero sobre lenguajes obscuros, pero no fueron bien-conocidas hasta que fueron adoptadas por lenguajes más populares. Lo que quiero hacer en esta sección es simplemente notar algunos hitos importantes, en medida de como fueron apareciendo en los lenguajes más-conocidos. Indudablemente el paso adelante mas importante fue el concepto de ¨lenguaje de alto-nivel¨, ejemplificados en Fortran y Cobol. Lo que estos lenguajes nos dieron fueron al menos tres nuevos principios: portabilidad de programas a través en un rango de máquinas; la habilidad de abordar nuevos problemas que eran muy grandes o dificultosos en lenguaje ensamblador; y la expansión de la fuente de programadores potenciales, antes que ese pequeño grupo con buena voluntad y disponibilidad para probar los obscuros misterios del funcionamiento que cada procesador individual tenía. Inútil sería decirlo, pero hubo quienes sentían que los ¨reales¨ programadores podrían continuar trabajando en lenguaje máquina. Aquellos ¨reales¨ programadores continuan entre nosotros, y siguen argumentando que sus habilidades especiales y virtudes superiores, de alguna forma compensan su pobre productividad. Las principales fallas de Fortran certeramente carecen de regularidad, estas fueron algunas molestas restricciones que en una mirada retrospectiva parecían ser innecesarias, además de algunas características que fueron impuestas más por las dependencias entre máquinas que por la conveniencia del programador. (La única justificación para el triple-camino es si Fortran II, por ejemplo, hubiera sido bien encajado dentro del lenguaje máquina en alguna máquina que ahora fuese obsoleta.) Algunas de estas fallas fueron corregidas en Algol 60, que en parte ha inspirado un gran número de lenguajes Algoleanos. El principal avance conceptual en Algol fue probablemente la introducción de archivación en estructuras de control, que permitió un control más limpio de tales estructuras. La revolución de la programación estructurada es considerada hacia la fecha de la famosa notación considerada perjudicial ¨GOTO¨ del científico Dijkstra. Aunque esto esté sobre-simplificado, es verdad que la realización de la contrucción del GOTO fue innecesaria - y siempre indeseada - fue así una importante parte de el descubrimiento de que la productividad en la programación estuvo mucho más relacionada a tener legibles y mejor-estructurados programas. El efecto de esto ha tenido en el diseño de lenguajes un nuevo énfasis en la ¨economía de concepto¨; que consiste en tener lenguajes que sean regulares en diseño, con la característica de eludir casos especiales y barrocos junto con las construcciones difíciles de leer. La importante contribución de Pascal se basó en extender las ideas de estructuras de control hacia estructuras de datos; a pesar de los variados mecanismos de estructuración de datos que ya existían en los tempranos lenguajes - incluso C tenía una forma de declarar estructuras constantes - Pascal juntó todas en un camino integral. Pascal puede continuar aún siendo considerado como un lenguaje viable, con un gran número de usuarios, pero al menos tiene dos faltas llamativas. Primero, fue estandarizado muy temprano, lo que significa algunos insignificantes defectos - los crudos arreglos de entrada/salida - estos jamás fueron arreglados en el lenguaje estándar. Fueron arreglados en algunas implementaciones de Pascal, pero las reparaciones van fuera del estándar y son por consiguiente no-portables. La segunda mayor falta es que un programa de Pascal debería (si uno quiere conformarlo hacia lo estándar) existir en un solo archivo, lo que transforma al lenguaje en inadecuado para la creación de largos programas. Más recientemente, hubo mucho énfasis en la salida de programas reusables y en un eficiente manejo de grandes programas. La idea clave aquí es la modularidad, y esto será discutido la siguiente sección. Ahora, en qué parte de la fotografía encaja C? La respuesta es que C ha sido construido mediante lecciones aprendidas desde Algol 60 y sus tempranos sucesores, y eso no incorpora mucho de lo aprendido desde entonces. Hemos aprendido algunas nuevas cosas acerca del diseño de lenguajes en los ultimos 20 años, y sabemos que algunas de las cosas que parecían ser buenas ideas al mismo tiempo parecieron luego ser no tan buenas. No es ya tiempo de movernos a D o a E? Modularidad En un sentido muy crudo, modularidad significa la posibilidad de moderar un gran programa hacia uno más pequeño, separadamente en secciones compiladas. C permite esto. Incluso Fortran II lo permitió. Esto, sin embargo, no es suficiente. La modularidad se trata realmente de la encapsulación y ocultación de datos. La idea esencial yace en que cada módulo debería hacerse cargo de una particular clase de datos, y no debería existir el camino de acceder a esos datos excepto mediante los procedimientos proveídos por el mismo módulo. Los detalles de implementación de las estructuras deberían ser ocultados. No debería existir la manera de llamar a un procedimiento al menos que el módulo explícitamente exporte tal procedimiento. Con mayor importancia, quien llame a un módulo no debería necesitar saber nada en lo absoluto de tal módulo excepto por las declaraciones y comentarios de su sección ¨visible¨. Debería ser posible el desarrollo de un módulo sin tener conocimiento alguno sobre la estructura interna de otros módulos. Las ventajas deberían ser obvias. En algun momento dado, un programador necesita solo atañerse de una corta sección de programa - típicamente de unas pocas páginas - despreocupándose así de efectos-laterales que puedan surgir en otros lugares dentro del programa. Es posible trabajar con complejas estructuras de datos sin tener la preocupación de saber sus detalles internos. Es posible reemplazar un módulo con una nueva versión - y esto además incluye la posibilidad de una completa revisión del camino donde es implementada la estructura de datos - sin tener que alterar o re-chequear los otros módulos. En una situación dentro de un equipo de programación, los problemas de coordinación se volverían mucho más simples. Si el hardware soporta segmentación de memoria, entonces los datos de cada módulo estarían protegidos de daños accidentales provenientes de otros módulos (excepto por la extensión de los punteros que son pasados como parámetros de procedimientos). Esto produce errores fáciles de detectar y arreglar. Además, sin protección de hardware, la incidencia de los errores de programación es reducida, porque las tazas de error dependen de la complejidad del programa, y un módulo de pocas páginas por lejos, será men os complejo que un monolítico programa de cien páginas. Ahora, la programación modular es posible en C, sólo si el programador se aferra a algunas justas y rígidas reglas: - Exactamente un sólo archivo (donde se declaran funciones(header)) debe existir por módulo. Tal archivo debería contener los prototipos de funciones y las declaraciones simbólicas(typedef) que se exportarán, y nada más (excepto comentarios). - Los comentarios del archivo header deben existir para que algun llamador externo tenga lo que necesita saber acerca del módulo. Los escritores no deberían tener la necesidad de saber sobre el módulo, excepto por el contenido del archivo header. - Todo módulo debe importar su propio archivo header, como chequeo de consistencia. - Cada módulo debería contener líneas #include para cualquier cosa que sea exportada desde otro módulo, junto con comentarios que demuestren lo que está siendo importado. Tales comentarios deben mantenerse actualizados. No debería haber dependencia sobre importaciones ocultas que ocurren como consecuencia de los empalmes de líneas #include que tipicamente aparecen cuando un archivo header necesita importar un tipo de definición o constante de algun otro lado. - Los prototipos de las funciones no deberían ser usados excepto en los archivos header. (Esta regla es necesaria porque C no tiene mecanismos para chequear el hecho de que una función sea implementada en el mismo módulo con su prototipo; entonces el uso de un prototipo puede enmascarar un error de función perdida). - Cada variable global de un módulo, y toda otra función que no sea alguna de las funciones exportadas por el archivo header, debería ser declarada estática. - La advertencia del compilador ¨llamada de función sin prototipo(function call without prototype)¨ debería ser habilitada, y cualquier advertencia debe ser tratada como un error. - Para cada prototipo dado en un archivo header, el programador debería chequear que una función no-privada (ej. no-estática, en la usual terminología del C) no tenga el mismo nombre como implementación en el mismo módulo. (Desafortunadamente, la naturaleza del lenguaje C hace que un chequeo automático sea imposible.) - Cualquier uso de grep debe ser visto con sospecha. Si un prototipo no estuviera en su obvio lugar, probablemente sea un error. - Idealmente, los programadores que trabajan en un equipo no deberían tener acceso los códigos fuente de sus compañeros. Solo deberían compartir módulos objeto y archivos header. Ahora, la obvia dificultad con esas reglas es que poca gente se aferrará a ellas, porque el compilador no los obliga a hacerlo. Una gran cantidad de personas piensan en #include como un mecanismo para esconder información, más que como un mecanismo para exportar información. (Esto es demostrado por la angustiosa práctica de escribir archivos header sin comentarios.) El significado intuitivo-contable de estático es un desincentivo para usarlo apropiadamente. Los prototipos de funciones tienden a ser arroja dos dentro de un programa en un modo fortuito, más que ser confinados hacia archivos header. Programadores que piensan en agregar comentarios luego de haber escrito el código aceptarán duramente la disciplina de guardar los comentarios en sus líneas #include actualizadas. Algunos buenos programadores prefieren deshabilitar los mensajes de advertencia en sus compilaciones, porque esto produce muchos mensajes no importantes que distraen. Finalmente, la noción de tener precisamente sólo un header por módulo, corre cuentas con las tradiciones que han sido construidas entre la comunidad de usuarios del C. Y, lo que es peor, se necesitaría un solo programador en un equipo para quebrantar la modularidad del proyecto, forzando al resto del equipo para que pierdan tiempo con grep y con misteriosos errores causados por inesperados efectos-laterales. Creo que esto es bien conocido en la mayoría de los equipos de programación que incluyen al menos un mal programador. Un lenguaje de programación modular proteje a los buenos programadores de la menor parte del caos causada por los malos programadores. Esto es algoque C no hace. Para complicar el asunto, es fácil hasta para los buenos programadores, violar, por accidente, las reglas de la propia modularidad. No hay mecanismo en C para forzar la regla de que cada prototipo mencionado en un header sea encontrado por una implementación del mismo módulo, o incluso para chequear que los nombres de función en el módulo implementado aparezcan en el archivo header. Es fácil olvidar hacer privadas las funciones internas, ya que el comportamiento original vuelve al frente: el original sirve para hacer todas las funciones exportables, esté o no usando un prototipo. Es fácil también, perder el hilo de lo que es importado y desde donde es importado, porque la información crucial es apartada en los comentarios que el compilador no chequea. El único camino que conozco para chequear lo que está siendo importado es comentando temporalmente las líneas #include, para ver que mensajes de error son producidos. En la mayoría de los programas modulares, todos o alguno de los módulos necesitarán una sección de inicialización. (No podemos inicializar estructuras de datos desde afuera del módulo, ya que se supone que son invisibles fuera del módulo.) Esto significa que el principio de un programa en C debe arreglarse para llamar a los procedimientos de inicialización en el orden correcto. El orden correcto es desde arriba: si el módulo MA depende del módulo MB, entonces MB debe ser inicializado antes de el módulo MA. Cualquier lenguaje que soporte programación modular se encargará de hacer esto por nosotros, y performará la inicialización en correcto orden. (También reportará dependencias circulares, que en la mayoría de los casos refleja esto en el diseño global del programa.) En C, tenemos que trabajar eso a mano, que puede ser un trabajo tedioso donde hay mas de una docena de módulos. En la práctica, he averiguado que es casi imposible obviar dependencias circulares en un largo programa de C, mientras que raramente entablo tales dependencias en programas de Modula-2. La razón es que la combinación compilador/(linker (establecedor de librerías)) encuentra circularidades de forma temprana, antes de que se vuelva demasiada dificultosa la tarea de re-diseñar el programa. En C, tales errores no se muestran hasta que misteriosos errores aparecen al final del testeo. Los peligros de #include He escuchado decir en algunas ocasiones que la directiva #include tiene en C esencialmente la misma funcionalidad que el IMPORT de Modula-2 y lenguajes similares. De hecho hay una profunda diferencia, que a continuación intentaré mostrar. Considerando que un archivo header m2.h contiene las lineas: #include /* DESDE m1 IMPORTAMOS stInfo */ void AddToQueue (stInfo* p); y supongamos que unos cuantos otros módulos contienen #include . Considerando la siguiente secuencia de eventos que podrían ocurrir fácilmente dentro de un proyecto de programación: (a) algunos de los módulos que se importan de m2 son compilados; (b) como resultado de un cambio en el diseño, el typedef que define stInfo en m1 es alterado; (c) los módulos restantes importados de m2 son compilados. En este punto, la globalidad del programa está en un estado inconsistente, ya que algunos de los módulos fueron compilados con una definición obsoleta; pero el error probablemente no sea cazado por el compilador o el linker. Si tu incluso continuas preguntandote por qué tienes que seguir compilando ¨todo¨ en lugar de eliminar una falla misteriosa, esto es parte de la razón. La razón de por qué esto ocurre en C, mientras no ocurra en lenguajes diseñados para programación modular, es que un header de C es puramente un archivo de texto, sin estar abastecido por una ¨compilación de último ¨momento¨ u otro para un chequeo de consistencia. (Esto también es así porqué los compiladores de C aparentan ser dolorosamente lentos cuando son comparados, por ejemplo, con un típico compilador de Modula-2. Este usualmente tarda más en leer un archivo header que en leer un archivo simbólico.) La repulsiva consecuencia de leer un archivo header, literalmente es que la información en el header es tratada como si hubiese estado en el archivo que contenía el #include. No hay un ¨firewall¨ que proteja el archivo header. Todo lo declarado en un header es automáticamente exportado, en efecto, hacia los header mencionados en cada #include. Esto puede liderar obscuros errores que dependen del orden de las líneas #include. También significa que el efecto de un header no está bajo el control total de la persona que lo escribió, ya que su comportamiento depende de lo que venga antes en el módulo que está siendo importado. Problemas similares existen con otras directivas preprocesadas, como #define. Este punto no es siempre entendido del todo: los efectos de un #define persisten a través de una compilación entera, incluyendo algunos archivos incluidos. No hay forma literal de declarar una constante local en C. Has tenido alguna vez la experiencia de obtener un reporte del compilador como error en una función de la librería que no has llamado, donde el real error se torna fuera de ser un punto y coma fuera de lugar en un archivo completamente no relacionado? Tales efectos no-locales se burlan de la modularidad. Otro problema con #include es que está en una proposición todo-o-nada. (Esto puede ser resuelto teniendo varios archivos header por módulo; pero eso significa poner los header bajo el control del importador, no el exportador, lo que crea el riesgo de discrepancias no-detectadas entre módulo y su archivo/s header. En algun caso, tal práctica crea mayores dolores de cabeza en términos de agendar y nombrar convenciones.) Cuántos programadores leen un archivo header en su totalidad antes de decidir si lo vana incluir? Muy pocos, supongo. La situación más cómun es que #include importa algunos nombres que el importador desconoce. Esto podría ser un desastre si, como a veces ocurre, dos funciones parecen tener el mismo nombre y los mismos tipos de parámetros. (Si tu piensas que esto es poco probable, sólo piensa en las versiones obsoletas de software que han dejado de existir, cuando tu copiabas archivos de lugar en lugar.) El compilador no se quejará, este simplemente asumirá que tu has estado en una expansiva genialidad y decidiste escribir dos veces un prototipo de función. El linker debería quejarse, pero no puedes garantizarlo. Como un resultado, es posible importar una función que es diferente de la función que creíamos haber importado, y no hay necesariamente un advertencia de error. Parte del problema aquí es que el mecanismo por el cual el linker elige que funciones unir, no tiene conexión alguna con el mecanismo por el cual el compilador chequea los prototipos de funciones. No hay modo de especificar que un archivo header en particular pertenezca a un modo en particular. Notemos, también, que un prototipo no-usado jamás es reconocido como un error. (Esta es otra razón para insistir que los prototipos sean usados sólo en los archivos header, y en níngun otro lado. Esto no soluciona el problema, pero reduce la cantidad de chequeos manuales que deben ser hechos.) Mientras esto no cause que un programa corra incorrectamente, será algo que se adicionará en la confusión que experimentarán los encargados de mantener el programa en un futuro. La velocidad del desarrollo de un programa Una afirmación algunas veces escuchada, es que el inicio del desarrollo de un programa es rápido en C porque es fácil hacer una ¨primera compilación limpia¨. (Esto es también un argumento que es popular con los entusiastas del BASIC.) Esta propiedad es constrastada con lo que pasa en lenguajes como Modula-2, donde - se dice que - es necesario mucho planeamiento por adelantado antes de que cualquier progreso sea hecho en la codificación. La conclusión es que los programadores de C obtienen mayor realimentación inmediata. Este argumento es tonto en al menos tres caminos. Primero, la afirmación depende al menos en parte de el hecho en que los compiladores de C son más generosos en aceptar código dudoso que los compiladores para lenguajes de más-alto-nivel. Donde está la virtud en eso? Si la compilación del código contiene errores, es visto como un insignificante paso adelante, eso se puede obtener en cualquier lenguaje. Todo lo que tienes que hacer es ignorar los mensajes de error. Segundo, la ¨primer compilación limpia¨ es justamente una insignificante medida de cuan lejos has progresado. Debería ser un significante avance si has programado hasta un lugar donde la codificación no halla comenzado a menos que la mayoría del diseño halla sido completado, pero solo sería significante de tal manera. Bajo la filosofía de programación ¨codifica, luego depura¨, continuas teniendo enfrente tuyo la mayoría de tu trabajo luego de la primera compilación. Finalmente, mi propia experiencia es que mientras la declaración original sea incorrecta. Encuentro que alcanzo la etapa de la ¨primera compilación limpia¨ dentro de los pocos minutos luego de haber empezado a trabajar. Esto es porque yo prefiero desarrollar programas a través de refinada prudencia (también conocida como diseño de vaciado(top-down design) combinado con codificación de vaciado. La primera cosa que he compilado consistía quizá en un código con media docena de líneas, junto con una docena más en líneas de comentarios. Eso es tan corto que se compilaría inmediatamente sin errores, ya que de esta forma sería fácil detectar en pocas líneas la existencia de algún error obvio. Qué hay acerca de la subdivisión de un largo programa en módulos? Esto toma mucho menos planeamiento de antemano como comunmente se supone. Con un refinamiento prudente, y con la filosofía de que la función de un módulo es ocuparse de un tipo de dato en especial, uno tiende a descubrir que módulos se necesitan a medida de que el programa se vaya desarrollando. Además, la verdadera modularidad hace que la construcción y el testeo del programa en etapas sea mucho más fácil, porque la propiedad de cambiar los detalles internos del módulo puede ser hecho independientemente de lo que ocurra fuera de tal módulo. En casos donde he mantenido una cuenta sobre el tiempo que he gastado en un proyecto, he concluido que en un programa de C he tardado aproximadamente el doble de tiempo de lo que me tomaría solucionar un problema de complejidad equivalente usando Modula-2. La diferencia no tiene nada que ver en lo que respecta a tipear rápido - ya que los códigos fuente tienden a ser de tamaño similar - la diferencia yace en el tiempo invertido en la depuración. En Modula-2, el trabajo está esencialmente completo una vezque he tipeado el último módulo, haciendo que los depuradores raramente sean necesitados. En C, un buen depurador es indispensable. Para el coordinador de un proyecto, esto es un importante factor. En un gran proyecto, el costo de pagarle a los programadores es tipicamente el segundo mayor presupuesto (después de los gastos administrativos), siendo a veces el mayor gasto. Una diferencia del 50% en la productividad puede marcar la diferencia entre producir con gran proficiencia o gran pérdida en el proyecto. Punteros: the GOTO of data structures /*--- El traductor no ha encontrado un modo de representar mediante esta traducción, la intención irónica que inspira el autor aprovechando el GOTO como herramienta de parafraseo ---*/ A pesar de todos los avances que han surgido en la teoría y práctica de las estructuras de datos, los punteros dejan espinas por todos lados. Algunos lenguajes (ej. Fortran, Lisp) se manejan sin punteros explícitos, pero al costo de complicar la representación de algunas estructuras de datos. (En Fortran, por ejemplo, hay que estimular todo usando vectores de punteros(arrays).) Para cualquiera que esté trabajando con una aplicación razonablemente avanzada, se le hace difícil el hecho de evitar el uso de punteros. Esto no significa que los punteros deben ser de nuestro agrado. Los punteros son responsables de una significante cantidad de tiempo al depurar (debugging), y de una larga proporción de complejidad que torna dificultoso el desarrollo de un programa. Un mayor reto para los diseñadores de software en lenguajes como Modula-2 es el de restringir las operaciones de los punteros a los módulos de bajo-nivel, entonces la gente que trabaja con el software no tiene que tratar con ellos. Un mayor y gran problema que no ha sido solucionado aún, es que los diseñadores de lenguajes encuentren mecanismos que salven a los programadores de tener la necesidad de usar punteros. Habiendo dicho esto, uno también podría decir que se podría pintar una distinción entre punteros esenciales y punteros inesenciales. Un puntero esencial, en el presente contexto, es un puntero que es requerido en orden de crear y mantener una estructura de datos. Por ejemplo, un puntero es necesario para relacionar a un elemento ¨en espera¨ con su sucesor. (El lenguaje debería o no llamarlo explícitamente un puntero, pero eso es un tema aparte. Cualquiera sea el lenguaje, debe haber algún camino para implementar la operación de ¨encontrar sucesor¨.) Un puntero no tan esencial sería el cual no fuere necesario como parte de implementar una estructura de datos. En un típico programa de C, los punteros inesenciales exceden en número, con respecto a los punteros esenciales, en una significante cantidad. Hay dos razones para esto. La primera es que las tradiciones del C dan coraje a los programadores para crear punteros incluso en lugares donde ya existen buenos métodos de acceso; por ejemplo, para saltar a través de los elementos de un array. (Deberíamos culpar al lenguaje por la persistencia de este mal hábito? No lo sé; Yo simplemente noto que esto prevalece más entre programadores de C que entre los que prefieren otros lenguajes.) La segunda razón es la regla de C, cuando dice que todos los parámetros de funciones deben ser pasados por un valor. Cuando necesites un parámetro equivalente a un VAR de Pascal o a un inout de Ada, la única solución es pasar un puntero. Este es un gran contribuidor a la ilegibilidad de los programas en C. (Para ser justos, debería admitirse que C++ al menos provee la solución para este problema.) Si la situación empeora se vuelve necesario pasar un puntero esencial como un párametro de entrada salida(inout). En este caso, un puntero de un puntero debería ser pasado a la función, lo cual es confuso incluso para los programadores más experimentados. Eficiencia en el tiempo de ejecución Aquí aparenta haber una creencia ampliamente difundida entre programadores del C, en la que se argumenta el hecho de que C es cercano al lenguaje máquina - y que un programa en C producirá código objeto más eficiente que un programa equivalente escrito en un lenguaje de alto-nivel. No estoy consciente de algún detallado estudio acerca de esta cuestión, pero he visto los resultados de algunos estudios informales comparando compiladores de Modula-2 y C. Los resultados fueron que el código producido por los compiladores de Modula-2 eran más rápidos y compactos que el código producido por compiladores de C. Esto no debería ser tomado como una respuesta definitiva, ya que los estudios no eran lo suficiente extensos, pero indican que los programas de C podrían no ser tan eficientes como generalmente se piensa. Creo que además he visto demandas - donde a esta altura no puedo entrar en detalles - pero parecen argumentar que los compiladores de C, producen mejor código que el código producido por un programador de ensamblador. He observado un fenómeno similar cuando testeaba mi compilador SGL hace algunos años. La razón en ese caso parecía ser que el compilador hizo razonablemente un buen trabajo en cosas como asignación de registros, mientras que uno puede sufrir lapsos de concentración en el momento de concentrarse demasiado en los finos detalles. La regla general parece ser que un compilador de lenguaje de alto-nivel estará fuera de performar un compilador de bajo-nivel, principalmente porque un compilador de lenguaje de alto-nivel tiene más ámbito de crear decisiones acerca de como generar el código. Si tu haces en C cosas como poner un puntero en un array más que usar subíndices(subscripts), estarías tomando esa decisión fuera del compilador. Tal acercamiento debería producir un código más eficiente, pero para estar seguros de eso, habría que conocer muy bien los tiempos de instrucciónes en la máquina que estemos usando, y acerca de las estrategias para la generación de código en nuestro compilador. En adición, la decisión es no-portable, potencialmente liderandonos hacia mayores ineficiencias en el caso de cambiarnos hacia otra máquina o hacia otra versión del compilador. Con mayor importancia, la velocidad de un programa tiende a depender más sobre - las estrategias globales adoptadas - sobre que clase de estructuras de datos usar - que clase de algoritmos usar, y así - que depender de asuntos de micro-eficiencia relacionados precisamente con el modo en que es escrito el código. Cuando se trabaja en un lenguaje de bajo-nivel como C, se vuelve difícil quedar encaminado hacia los asuntos globales. Es verdad que los compiladores de C producen mejor código, en algunos casos, que los compiladores de Fortran en los tempranos años '70. Esto era así por la relación muy cerrada entre el lenguaje C y el lenguaje ensamblador PDP-11. (Construcciones como *p++ en C tiene la misma justificación que los tres caminos del IF en Fortran II: estos explotan una especial característica del set arquitectónico estructural de un procesador en particular.) Si tu procesador no es un PDP-11, esta ventaja es perdida. Qué hay acerca de C++? Se supone que el lenguaje C trae algunas fallas de C, y lo hace hasta un cierto punto. Sin embargo, trae dos mayores desventajas. Es mucho más complejo de lo que necesita ser, lo que puede llevar a que los programadores ignoren las características extendidas tanto como también usar tales en forma inapropiada. El segundo problema es que el lenguaje intenta mantener compatibilidad con C, conservando así la mayoría de las inseguras características. Este segundo problema significa que la mayoría de las fallas discutidas en secciones tempranas son también fallas de C++. El típico chequeo continua siendo minimo, y se continúa permitiendole a los programadores, que produzcan locas y barrocas construcciónes difíciles de leer. Lo más extraño de todo, es que sigue sin existir un soporte para modularidad más allá del crudo mecanismo #include. Esto es un poco sorpresivo: programación modular y programación orientada a objetos complementan muy bien entre ellas; Todo el esfuerzo que los diseñadores del C++ debieron haber tenido para introducir las extensiones orientadas a objetos, es más bien decepcionante el que no hallan puesto un ligero esfuerzo extra que podría haber resultado en una mayor mejora del lenguaje. Algunas de las características relacionadas con la programación orientada a objetos son complicadas, y abiertas al desuso por parte de los programadores que no las comprenden totalmente. Por seguridad, preferiría ver que los programadores aprendan programación orientada a objetos usando limpias implementaciones (ej. Smalltalk, Modula-3) antes de perderse en C++. La habilidad para pasar parámentros de funciones por referencia es una yapa(bonus) definida, pero el mecanismo para hacer esto es una pérdida innecesaria. La única motivación que puedo ver para implementar en este camino, es satisfacer a los programadores de Fortran que pierden la construcción EQUIVALENTE. Sobrecarga de operador y función es una bendición mixta. En manos de un competente programador esto puede ser una mayor virtud; pero cuando son usadas por un mal programador podría causar caos. Me sentiría feliz acerca de esta característica si los compiladores de C++ tuvieran la forma de detectar cuando un mal programador esté sentado frente a un compilador. Si hubiera una discrepancia entre una función y su prototipo , sería un error, o es una sobrecarga deliberada? Mas comúnmente sería un error, pero en algunos tales casos el compilador de C++ asumirá optimisticamente. Uno tendría que ser un poco sospechoso en cuando a la mejora de un programa que aumente la probabilidad de errores indetectados. No estoy seguro de como sentirme frente a múltiples herencias. Es poderoso, en el mismo sentido en que goto lo es; pero és esa la clase de poder que queremos? Yo tengo una molesta sospecha de que en el futuro nuestras guías para una ¨limpia programación¨ incluirá la regla de que la herencia objetiva debería ser siempre restringida a una simple herencia. Sin embargo, estoy preparado para admitir que tal evidencia aún no yace sobre esta cuestión. En resumen, C++ introduce algunos nuevos problemas sin realmente resolver los problemas originales. Los diseñadores han optado continuar con la tradición de C que reza ¨casi todo debería ser legal¨; creo, que eso fue un error. Librerías vs. características del lenguaje Una de las populares características del C++ es el largo conjunto de funciones dentro de su librería con las cuales es distribuido. Esto es, en efecto, una característica deseada, pero esto no debería confundirse con las inherentes propiedades del lenguaje. Buenas librerías pueden ser escritas para cualquier lenguaje; y en un caso más razonable, los compiladores inducen a que uno haga llamadas a procedimientos ¨extranjeros¨ escritos en otros lenguajes. Más generalmente, la gente a veces dice que les gusta C porque les gustan las cosas como argc y argv, printf, etc. (Yo no lo digo - he tenido muchos problemas con printf, sscanf, y en la manera en que he sido forzado a escribir alternativos formatos de funciones de entrada/salida (I/O) - pero eso es un tema aparte.) En algunos casos, las funciones de su agrado, que son famosas en su característica de ¨portables¨ son peculiares a un compilador en particular, y no incluso mencionadas en cualquier C estándar que ellos consideran como estándar estándar. El deseo o no de varias rutinas de librería es un motivo legítimo para debatir, pero esto es un tema aparte de lo que serían las propiedades de un lenguaje. Hay sólo un camino en el cual esas funciones difieren como un resultado de genuinas diferencias de lenguaje, de los procedimientos disponibles en otros lenguajes, y eso es la regla del C que permite funciones con números variables y tipos de parámetros. Mientras esta característica tenga ciertas ventajas, esto necesariamente envolvería una relajación por parte del compilador a la hora de hacer chequeos de tipo. Personalmente he perdido horas de valioso tiempo al depurar sobre cosas como imprimir un dato long int con un formato apropiado para un dato int, y así vislumbrándome el descubrimiento del por qué mis computaciones fueron producidas con el valor erróneo. Hubiera sido mucho mas rápido, e incluso con vista más verbal, el haber llamado a procedimientos del tipo-seguro. Observaciones concluyentes Nada de este documento debería ser interpretado como una crítica de los originales diseñadores de C. Paso a creer que tal lenguaje fue una gran invensión para su época. Yo simplemente hago sugerencias sobre el tema de que han ocurrido algunos avances en el arte y ciencia del diseño de software desde aquellos tiempos, y que nosotros deberíamos tomar ventaja de tales avances. No soy tan ingenuo como para suponer que diatribas como esta provocarían la muerte del lenguaje. Lealtad hacia un lenguaje es un asunto ampliamente emocional el cual no sería motivo digno de un debate racional. Podría esperar, sin embargo, de que puedo convencer al menos pocas personas para que vuelvan a pensar sobre sus posiciones. Reconozco también, que factores diferentes a la inherente calidad de un lenguaje pueden ser importantes. La disponibilidad de compiladores es uno de tales factores. El re-uso de software existente también lo es; esto puede dictar el continuo uso de un lenguaje incluso cuando no es claramente la mejor opción en otras tierras. (En efecto, personalmente continúo usando el lenguaje para algunos proyectos, principalmente por esta razón.) Lo que necesitamos para resguardarnos, sin embargo, es hacer malas elecciones a través de la simple inercia. /*****************************************************************************************************************/