Creo que he mezclado código C y C ++ cuando no debería haberlo hecho; ¿Es esto un problema y cómo rectificar?

10

Antecedentes / escenario

Comencé a escribir una aplicación CLI únicamente en C (mi primer programa C o C ++ correcto que no era "Hello World" o una variación del mismo). A mitad de camino, estaba trabajando con "cadenas" de entrada de usuario (matrices de caracteres) y descubrí el objeto de cadena de C ++. Vi que podía guardar código usando estos, así que los usé a través de la aplicación. Esto significa que he cambiado la extensión del archivo a .cpp y ahora compilo la aplicación en g++lugar de gcc. Basándome en esto, diría que la aplicación ahora es técnicamente una aplicación C ++ (aunque el 90% + del código está escrito en lo que yo llamaría C, ya que hay muchos cruces entre los dos idiomas dada mi experiencia limitada de los dos). Es un único archivo .cpp de alrededor de 900 líneas de largo.

Factores importantes

Quiero que el programa sea gratuito (como en dinero) y libremente distribuible y utilizable para que todos lo tengan. Mi preocupación es que alguien vea el código y piense algo en el sentido de:

Mira la codificación, es horrible, este programa no puede ayudarme

Cuando potencialmente podría! Otra cuestión es que el código sea eficiente (es un programa para probar la conectividad Ethernet). No debe haber partes del código que sean tan ineficientes que puedan dificultar severamente el rendimiento de la aplicación o su salida. Sin embargo, creo que esa es una pregunta para Stack Overflow cuando solicito ayuda con funciones específicas, métodos, llamadas a objetos, etc.

Mi pregunta

Tener (en mi opinión) mezclado C y C ++ donde tal vez no debería. Debo buscar reescribirlo todo en C ++ (con esto, quiero decir implementar más objetos y métodos de C ++ en los que quizás haya codificado algo en un estilo de C que pueda condensarse usando técnicas más nuevas de C ++), o elimine el uso de objetos streamer de cadena y traer todo "de vuelta" al código C? ¿Hay un enfoque correcto aquí? Estoy perdido y necesito alguna orientación sobre cómo mantener esta aplicación "Buena" a los ojos de las masas, para que la usen y se beneficien de ella.

El Código - Actualización

Aquí hay un enlace al código. Es alrededor del 40% de comentarios, comento casi todas las líneas hasta que me siento más fluido. Sin embargo, en la copia a la que me he vinculado, he eliminado casi todos los comentarios. Espero que esto no haga que sea demasiado difícil de leer. Sin embargo, espero que nadie deba entenderlo completamente. Sin embargo, si he cometido fallas de diseño fatales, espero que puedan identificarse fácilmente. También debo mencionar que estoy escribiendo un par de computadoras de escritorio y portátiles Ubuntu. No tengo la intención de portar el código a otros sistemas operativos.

jwbensley
fuente
3
Desambigué CLI para ti. CLI también puede referirse a Common Language Infrastructure, que no tiene mucho sentido desde una perspectiva C.
Robert Harvey
Podrías reescribirlo en FORTRAN. Nunca oí hablar de OOF.
ott--
2
No me preocuparía demasiado si las personas no usan su software si no es "bonito". El 99% de sus usuarios ni siquiera mirarán el código ni se preocuparán por cómo está escrito, siempre y cuando cumplan con lo que necesitan. Es, sin embargo, es importante que el código sea consistente, etc, para ayudar a que en su mantenimiento a largo plazo.
Evicatos
1
¿Por qué no hace que su software sea libre (p. Ej. Bajo licencia GPLv3 ), con el código p . Ej . Github ? Tu código carece de un LICENSEarchivo. Puede obtener comentarios interesantes.
Basile Starynkevitch

Respuestas:

13

Comencemos por el principio: el código mixto C y C ++ es bastante común. Para empezar, estás en un gran club. Tenemos enormes bases de código C en la naturaleza. Pero por razones obvias, muchos programadores se niegan a escribir al menos cosas nuevas en C, al tener acceso a C ++ en el mismo compilador, los nuevos módulos comienzan a escribirse de esa manera, al principio dejando solo las partes existentes.

Luego, eventualmente, algunos archivos existentes se vuelven a compilar como C ++, y algunos puentes se pueden eliminar ... Pero puede llevar mucho tiempo.

Está algo adelantado, su sistema completo ahora es C ++, solo que la mayor parte está escrito "C-style". Y ve que la combinación de estilos es un problema, lo que no debería: C ++ es un lenguaje de paradigmas múltiples que admite muchos estilos y les permite coexistir para siempre. En realidad esa es la principal fortaleza, que no estás obligado a un solo estilo. Uno que sería subóptimo aquí y allá, con algo de suerte no en todas partes.

Volver a trabajar el código base es una buena idea, SI está roto. O si está en el camino del desarrollo. Pero si funciona (en el sentido original de la palabra), siga el principio de ingeniería más básico: si no está roto, no lo arregle. Deje las partes frías en paz, ponga su esfuerzo donde cuenta. En las partes que son malas, peligrosas, o en nuevas características, y simplemente refactorice las partes para convertirlas en una cama.

Si busca asuntos generales que abordar, esto es lo que vale la pena desalojar de una base de código C:

  • todas las funciones str * y char [] - reemplácelas con una clase de cadena
  • si usa sprintf, cree una versión que devuelva una cadena con el resultado, o la ponga en la cadena y reemplace el uso. (Si nunca se molestó con las transmisiones, hágase un favor y simplemente omítalas, a menos que le gusten; gcc proporciona una seguridad de tipo perfecta fuera de la caja para verificar formatos, simplemente agregue el atributo adecuado.
  • La mayoría de malloc y gratis: NO con nuevos y eliminar, sino vector, lista, mapa y otras colecciones.
  • el resto de la administración de memoria (después de los dos puntos anteriores, debe ser bastante raro, cubrir con punteros inteligentes o implementar sus colecciones especiales
  • reemplace todo otro uso de recursos (FILE *, mutex, lock, etc.) para usar envoltorios o clases RAII

Cuando haya terminado con eso, se acercará al punto en el que la base de código puede ser razonablemente segura para excepciones, por lo que puede soltar el fútbol de código de retorno utilizando excepciones y raros intentos / capturas solo en funciones de alto nivel.

Más allá de eso, simplemente escriba código nuevo en algunos C ++ sanos, y si nacen algunas clases que son un buen reemplazo en el código existente, recójalos.

No mencioné cosas relacionadas con la sintaxis, obviamente utilizo referencias en lugar de punteros en todo el código nuevo, pero reemplazar las viejas partes C solo para ese cambio no es un buen valor. Debes abordar, eliminar todo lo que puedas y usar variantes de C ++ en las funciones de contenedor para el resto. Y lo más importante, agregue constante siempre que sea aplicable. Estos se entrelazan con las balas anteriores. Y consolide sus macros y reemplace lo que puede convertir en enumeración, función en línea o plantilla.

Sugiero leer los estándares de codificación C ++ de Sutter / Alexandrescu si aún no se han hecho y seguirlos de cerca.

Balog Pal
fuente
Muchas gracias por el aporte de Balog, todos los buenos consejos en mis ojos y estoy de acuerdo con todo. Buscaré cambiar lentamente el código sección por sección, creo, priorizando el código de trabajo.
jwbensley
7

En resumen: no irás al infierno, no pasará nada malo, pero tampoco ganarás ningún concurso de belleza.

Si bien es perfectamente posible usar C ++ como una "mejor C", está descartando muchos de los beneficios de C ++, pero debido a que no se limita a Vanilla C, no obtiene ninguno de los beneficios de C (simplicidad, portabilidad , transparencia, interoperabilidad) tampoco. En otras palabras: C ++ sacrifica algunas de las cualidades de C para ganar otras, por ejemplo, la transparencia de C donde siempre se puede ver claramente dónde y cuándo ocurren las asignaciones de memoria se intercambia por las abstracciones más poderosas de C ++.

Dado que su código parece funcionar bien ahora, probablemente no sea una buena idea reescribirlo solo por el bien: manténgalo como está por ahora y cámbielo a C ++ más idiomático a medida que avanza, una pieza a la vez, cada vez que trabajas en una parte determinada. Y tenga en cuenta las lecciones así aprendidas para su próximo proyecto, sobre todo este: C y C ++ no son el mismo lenguaje, y es mejor que tome una decisión por adelantado que decidir cambiar a C ++ a la mitad de su proyecto .

tdammers
fuente
Gracias tdammers por el consejo. Estoy de acuerdo con lo que estás diciendo y lo estoy tomando en cuenta, ¡gracias!
jwbensley
4

C ++ es un lenguaje muy complejo, en el sentido de que tiene muchas características. También es un lenguaje que admite múltiples paradigmas, lo que significa que le permite escribir código de procedimiento completamente sin usar ningún objeto.

Entonces, debido a que C ++ es tan complejo, a menudo se ve que las personas usan un subconjunto limitado de sus características en su código. Los principiantes a menudo solo usan la E / S de flujo, los objetos de cadena y new / delete en lugar de malloc / free, y tal vez referencias en lugar de punteros. A medida que aprenda sobre las características orientadas a objetos, puede comenzar a escribir en un estilo llamado "C con clases". Eventualmente, a medida que aprende más sobre C ++, comienza a usar plantillas, RAII , STL , punteros inteligentes, etc.

El punto que estoy tratando de hacer es que aprender C ++ es un proceso que lleva tiempo. Sí, en este momento su código probablemente parece haber sido escrito por un programador de C que intenta escribir C ++. Y como solo estás aprendiendo, está perfectamente bien. Sin embargo, me da vergüenza, cuando veo este tipo de cosas en el código de producción escrito por programadores experimentados que deberían saberlo mejor.

Solo recuerde, un buen programador de Fortran puede escribir un buen código de Fortran en cualquier idioma. :)

Dima
fuente
2

Parece que está recapitulando exactamente el camino que tomaron muchos programadores de C antiguos al ir a C ++. Lo estás usando como una "mejor C". Hace veinte años esto tenía sentido, pero en este punto hay muchas construcciones más poderosas en C ++ (como la Biblioteca de plantillas estándar) que rara vez tiene sentido ahora. Como mínimo, es probable que debas hacer lo siguiente para evitar dar aneurismas a los programadores de C ++ al mirar tu código:

  • Utilice secuencias en lugar de? printffamilia.
  • Usar en newlugar de malloc.
  • Utilice las clases de contenedor STL para todas las estructuras de datos.
  • Usar en std::stringlugar de char*siempre que sea posible
  • Comprender y usar RAII

Si su programa es corto (y ~ 900 líneas parecen cortas), personalmente no creo que sea necesario o incluso útil intentar crear un conjunto robusto de clases.

Gort the Robot
fuente
Gracias por el consejo Steven, sí, tuve la misma idea sobre las clases y creo que puedo ordenar el código en un solo archivo ordenado. ¡Gracias!
jwbensley
2

La respuesta aceptada menciona solo las ventajas de convertir C a C ++ idiomático, como si el código C ++ fuera en algún sentido absoluto mejor que el código C. Estoy de acuerdo con otras respuestas en que probablemente no sea necesario hacer cambios drásticos en el código mixto si no está roto.

Que la mezcla de C y C ++ sea sostenible depende de cómo se realice la mezcla. Se pueden introducir errores sutiles, por ejemplo, cuando se generan excepciones en el código C (que no es seguro para excepciones), lo que provoca fugas de memoria o corrupción de datos. Una forma más segura y bastante común es envolver bibliotecas C o interfaces en clases, o usarlas en algún otro tipo de aislamiento en un proyecto C ++.

Antes de decidir reescribir todo su proyecto a C ++ idiomático (o C), debe tener en cuenta que muchos de los cambios presentados en otras respuestas pueden hacer que su programa sea más lento o causar otros efectos no deseados. Por ejemplo:

  • cambiar las cadenas C asignadas por pila a cadenas std :: puede causar asignaciones innecesarias de almacenamiento dinámico
  • el cambio de punteros sin formato a algunos tipos de puntero compartido (como std :: shared_ptr) provoca una sobrecarga de acceso debido al recuento de referencias, funciones de miembros virtuales internos y seguridad de subprocesos
  • las secuencias de biblioteca estándar son más lentas que las contrapartes de C
  • El uso descuidado de las clases RAII, como los contenedores, puede causar operaciones innecesarias, especialmente cuando no se puede usar la semántica de movimiento de C ++ 11
  • las plantillas pueden causar tiempos de compilación más largos, errores de compilación poco claros, hinchazón de código y problemas de portabilidad entre compiladores
  • escribir código seguro para excepciones es difícil, lo que facilita la introducción de errores sutiles durante la conversión
  • Es un truco usar código C ++ de una biblioteca compartida que C simple
  • C ++ no es tan ampliamente compatible como, por ejemplo, C89

Para concluir, la conversión de código C y C ++ mixto a C ++ idiomático debe verse como una compensación que tiene mucho más que solo tiempo de implementación y ampliar su caja de herramientas con características aparentemente convenientes. Por lo tanto, es muy difícil dar una respuesta para el caso general que no sea "depende".

crafn
fuente
"las secuencias de biblioteca estándar son más lentas que las contrapartes C": Probablemente, ciertamente más difícil para la internacionalización que printf de todos modos.
Deduplicador
1

Es una mala forma de mezclar C y C ++. Son idiomas distintos y realmente deberían tratarse como tales. Elija el que le resulte más cómodo e intente escribir código idiomático en ese idioma.

Si C ++ te está ahorrando mucho código, entonces quédate con C ++ y reescribe las partes de C. Dudo que una diferencia de rendimiento sea notable, si eso es lo que le preocupa.

Oleksi
fuente
Así que tengo esta biblioteca que quiero usar que está escrita en C, y tengo esta otra biblioteca que quiero usar que está escrita en C ++ ... Reescribir el código que funciona bien es solo crear trabajo innecesario.
gnasher729
1

Voy y vengo entre C y C ++ todo el tiempo y soy del tipo extraño que prefiere trabajar de esta manera. Prefiero C ++ para cosas de nivel superior, mientras que uso C como un instrumento contundente que me permite rayos X tipos de datos y tratarlos como bits y bytes con, digamos, memcpyque encuentro útil para implementar estructuras de datos de bajo nivel y asignadores de memoria . Si se encuentra trabajando a nivel de bits y bytes sin procesar, entonces el sistema de tipos realmente rico de C ++ no ayuda en absoluto, y a menudo me resulta más fácil escribir dicho código en C donde puedo asumir con seguridad que puedo tratar cualquier tipo de datos C como solo bits y bytes.

Lo principal a tener en cuenta es donde estos dos idiomas no encajan bien.

1. La función AC nunca debe llamar a una función C ++ que pueda throw. Dado cuántos lugares su código diario de C ++ puede encontrarse implícitamente en una excepción, eso generalmente significa que sus funciones C no deberían llamar a funciones C ++ en general. De lo contrario, su código C no podrá liberar los recursos que asigna durante el despliegue de la pila, ya que no tiene una forma práctica de detectar la excepción de C ++. Si termina requiriendo que su código C llame a una función C ++ como devolución de llamada, por ejemplo, el lado C ++ debe asegurarse de que cualquier excepción que encuentre se detecte antes de volver al código C.

2. Si escribe código C que trata los tipos de datos como solo bits y bytes sin procesar, arrasando sobre el sistema de tipos (esta es realmente mi razón principal para usar C en primer lugar), entonces nunca querrá usar dicho código contra datos C ++ tipos que podrían tener constructores y destructores de copias y punteros virtuales y cosas por el estilo que deben respetarse. Por lo tanto, generalmente debería ser un código C que usa un código C de este tipo, no un código C ++ que usa un código C de este tipo.

Si desea que su código C ++ use dicho código C, entonces generalmente desea usarlo como el detalle de implementación de una clase que se asegura de que los datos que está almacenando en la estructura de datos C genéricos son un tipo de datos que es trivial de construir y destruir Afortunadamente, hay rasgos de tipo como este que puede usar para verificar eso en una afirmación estática, asegurándose de que los tipos que está almacenando en la estructura de datos C genéricos tengan destructores y constructores triviales y se mantendrán así.

Debo buscar reescribirlo todo en C ++ (con esto, quiero decir implementar más objetos y métodos de C ++ en los que quizás haya codificado algo en un estilo de C que pueda condensarse usando técnicas más nuevas de C ++), o elimine el uso de objetos streamer de cadena y traer todo "de vuelta" al código C?

En mi opinión, no necesita molestarse si se adhiere a las reglas anteriores y se asegura de que su código esté bien probado contra las pruebas que ha escrito. La parte que podría valer la pena mejorar es el código que ha escrito en C ++ hasta ahora, pero eso no significa que tenga que portar el código que está estrictamente basado en C a C ++.

Con el código que ha escrito en C ++ y compilado como código C ++, debe tener cuidado. Usar codificación similar a C en C ++ es pedir problemas. Por ejemplo, si asigna y libera recursos manualmente, lo más probable es que su código no sea seguro, ya que su código podría encontrar una excepción en cuyo punto salga implícitamente de la función antes de que pueda utilizar freeel recurso. En C ++, RAII y cosas como los punteros inteligentes no son una conveniencia esponjosa, ya que pueden aparecer si solo mira las rutas de ejecución normales sin considerar rutas excepcionales. A menudo son una necesidad fundamental para escribir un código correcto seguro de excepciones de manera simple y efectiva que no solo comenzará a filtrarse por todas partes cuando se encuentre con una excepción.


fuente