C++ vs. D, Ada y Eiffel(mensajes de error horribles con plantillas)
templates generic-programming (6)
Uno de los problemas de C ++ son los mensajes de error horribles que recibimos del código que usa de forma intensiva plantillas y metaprogramación de plantillas. Los conceptos están diseñados para resolver este problema, pero desafortunadamente no estarán en el siguiente estándar.
Me pregunto si este problema es común para todos los idiomas, que respaldan la programación genérica. O algo está mal con las plantillas de C ++?
Lamentablemente, no conozco ningún otro lenguaje que admita la programación genérica (los genéricos Java y C # son demasiado simplificados y no tan potentes como las plantillas C ++).
Así que les pregunto chicos: ¿las plantillas D, Ada, Eiffel (genéricos) también producen mensajes de error tan feos? ¿Y es posible tener un lenguaje con un potente paradigma de programación genérico, pero sin mensajes de error desagradables? Y si es así, ¿cómo estos idiomas están resolviendo este problema?
Editar: para downvoters. Realmente amo C ++ y plantillas. No digo que las plantillas sean malas. De hecho, soy un gran admirador de la programación genérica y la metaprogramación de plantillas. Solo estoy preguntando por qué recibo mensajes de error tan desagradables de los compiladores.
D tiene dos características para mejorar la calidad de los mensajes de error de la plantilla: Restricciones y afirmación static assert
.
// Use constraints to only allow a function to operate on random access
// ranges as defined in std.range. If something that doesn''t satisfy this
// is passed, the compiler will error before even trying to instantiate
// fun().
void fun(R)(R range) if(isRandomAccessRange!(R)) {
// Do stuff.
}
// Use static assert to check a high level invariant. If
// the predicate is false, the error message will be
// printed and compilation will stop before a screen
// worth of more confusing errors are encountered.
// This function takes any number of ranges to merge sort
// and the same number of temporary buffers to merge into.
void mergeSort(R...)(R ranges) {
static assert(R.length % 2 == 0,
"Must have equal number of ranges to be sorted and temporary buffers.");
static assert(allSatisfy!(isRandomAccessRange, R),
"All arguments to mergeSort must be random access ranges.");
// Implementation
}
El artículo Generic Programming describe muchos de los pros y los contras de los genéricos en varios idiomas, incluido Ada en particular . Aunque carecen de especialización de plantilla, todas las instancias genéricas de Ada son "equivalentes a la declaración de instancia ... seguida inmediatamente por el cuerpo de la instancia". Como una cuestión práctica, los mensajes de error tienden a ocurrir en tiempo de compilación, y típicamente representan violaciones familiares de la seguridad de tipo.
Hay algunos esfuerzos para mejorar los mensajes de error. Clang , por ejemplo, ha puesto bastante énfasis en la generación de mensajes de error de compilación más fáciles de leer. Solo lo he usado por un tiempo, pero mi experiencia hasta el momento ha sido bastante positiva en comparación con los errores equivalentes de GCC.
En general, encontré que los mensajes de error del compilador Ada para los genéricos realmente no son mucho más difíciles de leer que cualquier otro mensaje de error del compilador de Ada.
Los mensajes de error de la plantilla de C ++, por otro lado, son notorios por ser novelas de error . La principal diferencia, creo, es la forma en que C ++ realiza la instanciación de plantillas. El caso es que las plantillas de C ++ son mucho más flexibles que los genéricos de Ada. Es tan flexible, es casi como un macroprocesador. La gente inteligente de Boost lo ha utilizado para implementar cosas como lambdas e incluso otros idiomas completos.
Debido a esa flexibilidad, la jerarquía de la plantilla completa básicamente tiene que compilarse de nuevo cada vez que se encuentra por primera vez su permutación particular de los parámetros de la plantilla. Por lo tanto, los problemas que resuelven las incompatibilidades de varias capas de una API terminan siendo presentados al cliente API pobre para descifrarlos.
En Ada, los genéricos en realidad están fuertemente tipados y brindan información completa al cliente, al igual que los paquetes normales y las subrutinas. Por lo tanto, si obtiene un mensaje de error, generalmente solo hace referencia al genérico que está tratando de instalar, no a toda la jerarquía utilizada para implementarlo.
Entonces, sí, los mensajes de error de la plantilla de C ++ son mucho peores que los de Ada.
Ahora la depuración es una historia completamente diferente ...
El problema, en el fondo, es que la recuperación de errores es difícil, sea cual sea el contexto.
¡Y cuando factorizas las gramáticas horribles de C y C ++, solo puedes preguntarte que los mensajes de error no son peores que eso! Me temo que la gramática C ha sido diseñada por personas que no tenían ni idea de las propiedades esenciales de una gramática, una de ellas es que cuanto menos se confía en el contexto, mejor y el otro es lo que debe esforzarse por hacer lo más inequívoco posible.
Vamos a ilustrar un error común: olvidar un punto y coma.
struct CType {
int a;
char b;
}
foo
bar() { /**/ }
De acuerdo, entonces esto está mal, ¿dónde debería ir el punto y coma faltante? Bueno, desafortunadamente es ambiguo, puede ir antes o después de foo
porque:
- C considera que es normal declarar una variable en zancada después de definir una
struct
- C considera normal no especificar un tipo de retorno para una función (en cuyo caso se predetermina a
int
)
Si razonamos, podríamos ver que:
- si
foo
nombra un tipo, entonces pertenece a la declaración de la función - si no, probablemente denote una variable ... a menos que, por supuesto, hiciéramos un error tipográfico y fuera escrito
fool
, que resulta ser un tipo: /
Como puede ver, la recuperación de errores es francamente difícil, porque necesitamos inferir lo que el escritor quiso decir, y la gramática está lejos de ser receptiva. Sin embargo, no es imposible, y la mayoría de los errores pueden diagnosticarse más o menos correctamente, e incluso recuperarse de ... solo requiere un esfuerzo considerable .
Parece que las personas que trabajan en gcc
están más interesadas en producir código rápido (y quiero decir rápido, buscar los últimos puntos de referencia en gcc 4.6) y agregar características interesantes (gcc ya implementa la mayoría - si no todos - de C ++ 0x) produciendo mensajes de error fáciles de leer. ¿Puedes culparlos? No puedo
Afortunadamente, hay personas que piensan que un informe preciso de errores y una buena recuperación de errores son un objetivo muy valioso, y algunos de ellos han estado trabajando en CLang durante un buen tiempo, y continúan haciéndolo.
Algunas características agradables, fuera de mi cabeza:
- Termine pero complete los mensajes de error, que incluyen los rangos de origen para exponer exactamente de dónde emanó el error.
- Notas de Fix-It cuando es obvio lo que se quiso decir
- En cuyo caso, el compilador analiza el resto del archivo como si la corrección ya hubiera estado allí, en lugar de arrojar líneas sobre líneas de galimatías
- (reciente) evite incluir la pila de inclusión para notas, para cortar el cruft
- (reciente) tratando solo de exponer los tipos de parámetros de la plantilla que el desarrollador realmente escribió, y preservando typedefs (hablando de
std::vector<Name>
lugar destd::vector<std::basic_string<char, std::allocator<char>>, std::allocator<std::basic_string<char, std::allocator<char>> >
que marca la diferencia) - (reciente) recuperándose correctamente en caso de que falte una
template
en caso de que falte en una llamada a un método de plantilla desde otro método de plantilla
Pero cada uno de ellos ha requerido varias horas a días de trabajo.
Ciertamente no vinieron gratis.
Ahora, los conceptos deberían haber (normalmente) hecho nuestras vidas más fáciles. Pero en su mayoría no habían sido probados, por lo que se consideró preferible eliminarlos del calado. Debo decir que estoy contento por esto. Dada la relativa inercia de C ++, es mejor no incluir características que no se han revisado completamente, y los mapas conceptuales realmente no me emocionaron. Tampoco les gustó Bjarne o Herb, ya que dijeron que iban a reconsiderar conceptos desde cero para el siguiente estándar.
Eiffel tiene el mejor de todos los mensajes de error porque tiene el mejor de todos los sistemas de plantillas. Está completamente integrado en el lenguaje y funciona bien porque es el único lenguaje que usa covarianz en argumentos.
Por lo tanto, es mucho más que un simple compilador copiar y pegar. Lamentablemente, explicar la diferencia en unas pocas líneas es imposible. Solo eche un vistazo a EiffelStudio.