Joel Spolsky caracterizó a C ++ como "suficiente cuerda para ahorcarse" . En realidad, estaba resumiendo "C ++ efectivo" de Scott Meyers:
Es un libro que básicamente dice: C ++ es suficiente cuerda para ahorcarse, y luego un par de millas extra de cuerda, y luego un par de píldoras suicidas que se disfrazan de M&M ...
No tengo una copia del libro, pero hay indicios de que gran parte del libro se relaciona con problemas de gestión de la memoria que parecen ser discutibles en C # porque el tiempo de ejecución maneja esos problemas por usted.
Aquí están mis preguntas:
- ¿C # evita las trampas que se evitan en C ++ solo mediante una programación cuidadosa? Si es así, ¿en qué medida y cómo se evitan?
- ¿Hay nuevas dificultades diferentes en C # que un nuevo programador de C # debe tener en cuenta? Si es así, ¿por qué no podrían evitarse con el diseño de C #?
c#
programming-languages
c++
alx9r
fuente
fuente
Your questions should be reasonably scoped. If you can imagine an entire book that answers your question, you’re asking too much.
. Creo que esto califica como tal pregunta ...Respuestas:
La diferencia fundamental entre C ++ y C # proviene del comportamiento indefinido .
No tiene nada que ver con la gestión manual de la memoria. En ambos casos, ese es un problema resuelto.
C / C ++:
En C ++, cuando comete un error, el resultado no está definido.
O, si intenta hacer ciertos tipos de suposiciones sobre el sistema (por ejemplo, desbordamiento de entero con signo), es probable que su programa no esté definido.
Tal vez lea esta serie de 3 partes sobre comportamiento indefinido.
Esto es lo que hace que C ++ sea tan rápido: el compilador no tiene que preocuparse por lo que sucede cuando las cosas salen mal, por lo que puede evitar verificar la corrección.
C #, Java, etc.
En C #, tiene la garantía de que muchos errores explotarán en su cara como excepciones, y le garantizamos mucho más sobre el sistema subyacente.
Esa es una barrera fundamental para hacer que C # sea tan rápido como C ++, pero también es una barrera fundamental para hacer que C ++ sea seguro, y hace que C # sea más fácil de trabajar y depurar.
Todo lo demás es simplemente salsa.
fuente
La mayoría lo hace, algunas no. Y, por supuesto, hace algunos nuevos.
Comportamiento indefinido : el mayor obstáculo con C ++ es que hay una gran cantidad de lenguaje indefinido. El compilador literalmente puede hacer explotar el universo cuando haces estas cosas, y estará bien. Naturalmente, esto es poco común, pero es bastante común que su programa funcione bien en una máquina y realmente no tiene una buena razón para no funcionar en otra. O peor, actuar sutilmente diferente. C # tiene algunos casos de comportamiento indefinido en su especificación, pero son raros y en áreas del lenguaje que se utilizan con poca frecuencia. C ++ tiene la posibilidad de encontrarse con un comportamiento indefinido cada vez que realiza una declaración.
Fugas de memoria : esto es menos preocupante para C ++ moderno, pero para principiantes y durante aproximadamente la mitad de su vida útil, C ++ hizo que sea muy fácil perder memoria. C ++ efectivo vino alrededor de la evolución de las prácticas para eliminar esta preocupación. Dicho esto, C # todavía puede perder memoria. El caso más común con el que se topan las personas es la captura de eventos. Si tiene un objeto y coloca uno de sus métodos como un controlador para un evento, el propietario de ese evento debe tener GC para que el objeto muera. La mayoría de los principiantes no se dan cuenta de que el controlador de eventos cuenta como referencia. También hay problemas al no disponer de recursos desechables que pueden perder memoria, pero estos no son tan comunes como los punteros en C ++ pre-efectivo.
Compilación : C ++ tiene un modelo de compilación retardado. Esto lleva a una serie de trucos para jugar bien con él, y mantener bajos los tiempos de compilación.
Cadenas : Modern C ++ lo hace un poco mejor, pero
char*
es responsable del ~ 95% de todas las infracciones de seguridad antes del año 2000. Para los programadores experimentados, se centraránstd::string
, pero aún es algo para evitar y un problema en las bibliotecas antiguas / peores . Y eso es rezar para que no necesites soporte Unicode.Y realmente, esa es la punta del iceberg. El problema principal es que C ++ es un lenguaje muy pobre para principiantes. Es bastante inconsistente, y muchos de los viejos y muy malos escollos han sido resueltos cambiando los modismos. El problema es que los principiantes necesitan aprender las expresiones idiomáticas de algo como Effective C ++. C # elimina muchos de estos problemas por completo y hace que el resto sea menos preocupante hasta que avance en el camino del aprendizaje.
Mencioné el problema del evento "pérdida de memoria". Esto no es un problema de lenguaje, ya que el programador espera algo que el lenguaje no puede hacer.
Otra es que el finalizador para un objeto C # no está técnicamente garantizado para ser ejecutado por el tiempo de ejecución. Esto generalmente no importa, pero hace que algunas cosas se diseñen de manera diferente de lo que cabría esperar.
Otro problema que he visto a los programadores es la semántica de captura de funciones anónimas. Cuando captura una variable, captura la variable . Ejemplo:
No hace lo que ingenuamente se piensa. Esto imprime
10
10 veces.Estoy seguro de que hay muchos otros que estoy olvidando, pero el problema principal es que son menos penetrantes.
fuente
char*
. Sin mencionar que todavía puede perder memoria en C # muy bien.enable_if
En mi opinión, los peligros de C ++ son algo exagerados.
El peligro esencial es este: mientras C # le permite realizar operaciones de puntero "inseguras" utilizando
unsafe
palabra clave, C ++ (que en su mayoría es un superconjunto de C) le permitirá usar punteros cuando lo desee. Además de los peligros habituales inherentes al uso de punteros (que son los mismos con C), como pérdidas de memoria, desbordamientos de búfer, punteros colgantes, etc., C ++ presenta nuevas formas para que arruines las cosas.Esta "soga extra", por así decirlo, de la que Joel Spolsky estaba hablando , básicamente se reduce a una cosa: escribir clases que manejan internamente su propia memoria, también conocida como la " Regla de 3 " (que ahora se puede llamar la Regla de 4 o regla de 5 en C ++ 11). Esto significa que si alguna vez desea escribir una clase que administre sus propias asignaciones de memoria internamente, debe saber lo que está haciendo o su programa probablemente se bloqueará. Debe crear cuidadosamente un constructor, un constructor de copia, un destructor y un operador de asignación, que es sorprendentemente fácil de equivocar, lo que a menudo resulta en extraños bloqueos en el tiempo de ejecución.
SIN EMBARGO , en la programación real de C ++ todos los días, es muy raro escribir una clase que administre su propia memoria, por lo que es engañoso decir que los programadores de C ++ siempre deben ser "cuidadosos" para evitar estos escollos. Por lo general, solo harás algo más como:
Esta clase se parece bastante a lo que haría en Java o C #: no requiere una administración de memoria explícita (porque la clase de la biblioteca
std::string
se encarga de todo eso automáticamente), y no se requiere nada de "Regla de 3" desde el valor predeterminado El constructor de copia y el operador de asignación están bien.Solo cuando intentas hacer algo como:
En este caso, puede ser complicado para los principiantes obtener la asignación, el destructor y el constructor de copia correctos. Pero para la mayoría de los casos, no hay razón para hacer esto. C ++ hace que sea muy fácil evitar la gestión manual de la memoria el 99% del tiempo mediante el uso de clases de biblioteca como
std::string
ystd::vector
.Otro problema relacionado es la gestión manual de la memoria de una manera que no tiene en cuenta la posibilidad de que se produzca una excepción. Me gusta:
Si
some_function_which_may_throw()
en realidad no lanzar una excepción, uno se queda con una pérdida de memoria debido a que la memoria asignada paras
que nunca se recuperó. Pero, de nuevo, en la práctica esto ya no es un problema por la misma razón por la que la "Regla de 3" ya no es realmente un problema. Es muy raro (y generalmente innecesario) administrar su propia memoria con punteros sin formato. Para evitar el problema anterior, todo lo que debe hacer es usar unstd::string
ostd::vector
, y el destructor se invocará automáticamente durante el desenrollado de la pila después de que se haya lanzado la excepción.Por lo tanto, un tema general aquí es que muchas características de C ++ que no se heredaron de C, como la inicialización / destrucción automática, los constructores de copias y las excepciones, obligan a un programador a tener mucho cuidado al hacer la gestión manual de la memoria en C ++. Pero, de nuevo, esto es solo un problema si tiene la intención de hacer una gestión manual de la memoria en primer lugar, lo que casi nunca es necesario cuando se tienen contenedores estándar y punteros inteligentes.
Por lo tanto, en mi opinión, aunque C ++ le proporciona una gran cantidad de soga adicional, casi nunca es necesario usarlo para ahorcarse, y las trampas de las que Joel hablaba son trivialmente fáciles de evitar en C ++ moderno.
fuente
Does C# avoid pitfalls that are avoided in C++ only by careful programming?
. La respuesta es "en realidad no, porque es trivialmente fácil evitar las trampas de las que Joel hablaba en C ++ moderno"Realmente no estaría de acuerdo. Quizás menos dificultades que C ++ tal como existía en 1985.
Realmente no. Reglas como la Regla de tres han perdido importancia masiva en C ++ 11 gracias a
unique_ptr
yshared_ptr
siendo estandarizadas. Usar las clases estándar de una manera vagamente sensata no es "codificación cuidadosa", es "codificación básica". Además, la proporción de la población de C ++ que sigue siendo lo suficientemente estúpida, desinformada o ambas para hacer cosas como el manejo manual de la memoria es mucho menor que antes. La realidad es que los profesores que desean demostrar reglas como esa tienen que pasar semanas tratando de encontrar ejemplos donde aún se aplican, porque las clases estándar cubren prácticamente todos los casos de uso imaginables. Muchas técnicas efectivas de C ++ han seguido el mismo camino: el camino del dodo. Muchos de los otros no son realmente tan específicos de C ++. Déjame ver. Saltando el primer elemento, los siguientes diez son:final
yoverride
he ayudado a cambiar este juego en particular para mejor. Haga su destructoroverride
y le garantiza un buen error de compilación si hereda de alguien que no hizo su destructorvirtual
. Haga su clasefinal
y no puede aparecer un matorral pobre y heredar de ella accidentalmente sin un destructor virtual.Obviamente no voy a revisar todos los elementos de Effective C ++, pero la mayoría de ellos simplemente están aplicando conceptos básicos a C ++. Encontraría el mismo consejo en cualquier lenguaje de operador sobrecargado orientado a objetos con tipo de valor. Los destructores virtuales son casi el único que es una trampa de C ++ y aún es válido, aunque, posiblemente, con la
final
clase de C ++ 11, no es tan válido como lo fue. Recuerde que Effective C ++ se escribió cuando la idea de aplicar OOP, y las características específicas de C ++, todavía era muy nueva. Estos elementos apenas tratan sobre las trampas de C ++ y más sobre cómo hacer frente al cambio de C y cómo usar OOP correctamente.Editar: Las trampas de C ++ no incluyen cosas como las trampas de
malloc
. Quiero decir, para uno, cada error que puede encontrar en el código C también puede encontrarlo en un código C # inseguro, por lo que no es particularmente relevante, y en segundo lugar, solo porque el Estándar lo define para la interoperación no significa que su uso se considere C ++ código. El estándar también lo definegoto
, pero si tuviera que escribir una pila gigante de espagueti con él, lo consideraría su problema, no el idioma. Hay una gran diferencia entre "codificación cuidadosa" y "Seguir modismos básicos del idioma".using
apesta Realmente lo hace. Y no tengo idea de por qué no se hizo algo mejor. Además,Base[] = Derived[]
y casi todos los usos de Object, que existe porque los diseñadores originales no notaron el éxito masivo de las plantillas en C ++, y decidieron que "Hagamos que todo herede de todo y perdamos toda la seguridad de nuestro tipo" fue la elección más inteligente . También creo que puedes encontrar algunas sorpresas desagradables en cosas como las condiciones de carrera con los delegados y otras cosas divertidas. Luego hay otras cosas generales, como cómo los genéricos apestan horriblemente en comparación con las plantillas, la colocación forzada realmente innecesaria de todo en unaclass
, y esas cosas.fuente
malloc
no significa que debas hacerlo, más que solo porque puedesgoto
ser una puta como una perra, significa que es una cuerda con la que puedes ahorcarte.unsafe
en C #, lo cual es igual de malo. También podría enumerar todas las trampas de codificar C # como C, si lo desea.C # tiene las ventajas de:
char
,string
, etc. es definido por la implementación. El cisma entre el enfoque de Windows para Unicode (wchar_t
para UTF-16,char
para "páginas de códigos" obsoletas) y el enfoque * nix (UTF-8) causa grandes dificultades en el código multiplataforma. C #, OTOH, garantiza que astring
es UTF-16.Si:
IDisposable
Hay un libro llamado Effective C # que es similar en estructura a Effective C ++ .
fuente
No, C # (y Java) son menos seguros que C ++
C ++ es localmente verificable . Puedo inspeccionar una sola clase en C ++ y determinar que la clase no pierde memoria u otros recursos, suponiendo que todas las clases referenciadas sean correctas. En Java o C #, es necesario verificar cada clase referenciada para determinar si requiere algún tipo de finalización.
C ++:
C#:
C ++:
C#:
fuente
auto_ptr
(o algunos de sus familiares). Esa es la cuerda proverbial.auto_ptr
es tan simple como saber usarIEnumerable
o saber usar interfaces, o no usar coma flotante para moneda o algo así. Es una aplicación básica de DRY. Nadie que conozca los conceptos básicos de cómo programar cometerá ese error. A diferenciausing
. El problemausing
es que debe saber para cada clase si es desechable (y espero que nunca cambie), y si no es desechable, automáticamente prohíbe todas las clases derivadas que podrían ser desechables.Dispose
método, debe implementarloIDisposable
(la forma 'adecuada'). Si su clase hace eso (que es el equivalente a implementar RAII para su clase en C ++), y usausing
(que es como los punteros inteligentes en C ++), todo funciona perfectamente. El finalizador está destinado principalmente a prevenir accidentes:Dispose
es responsable de la corrección, y si no lo está utilizando, bueno, es su culpa, no de C #.Sí, 100% sí, ya que creo que es imposible liberar memoria y usarla en C # (suponiendo que se administre y no entre en modo inseguro).
Pero si sabes programar en C ++, lo que no hace un número increíble de personas. Estás bastante bien. Al igual que las clases de Charles Salvia, realmente no manejan sus recuerdos, ya que todo se maneja en clases STL preexistentes. Raramente uso punteros. De hecho, fui a proyectos sin usar un solo puntero. (C ++ 11 hace esto más fácil).
En cuanto a cometer errores tipográficos, errores tontos, etc. (por ejemplo:
if (i=0)
bc, la clave se atascó cuando presionó == realmente rápido) el compilador se queja, lo cual es bueno, ya que mejora la calidad del código. Otro ejemplo es olvidar lasbreak
declaraciones de cambio y no permitirle declarar variables estáticas en una función (que a veces no me gusta, pero es una buena idea).fuente
=
/==
al usar==
para la igualdad de referencia e introducir la.equals
igualdad de valor. El programador pobre ahora tiene que hacer un seguimiento de si una variable es 'doble' o 'Doble' y asegurarse de llamar a la variante correcta.struct
puedes hacer lo==
que funciona increíblemente bien ya que la mayoría de las veces solo se tienen cadenas, ints y flotantes (es decir, solo miembros de estructura). En mi propio código, nunca tengo ese problema, excepto cuando quiero comparar matrices. Creo que nunca comparo los tipos de lista o no estructura (string, int, float, DateTime, KeyValuePair y muchos otros)==
para igualdad de valor yis
para igualdad de referencia.