¿Por qué los 'void *' 'no se emiten implícitamente en C ++?

30

En C, no hay necesidad de lanzar un void *a ningún otro tipo de puntero, siempre se promociona de forma segura. Sin embargo, en C ++, este no es el caso. P.ej,

int *a = malloc(sizeof(int));

funciona en C, pero no en C ++. (Nota: Sé que no debe usar mallocen C ++, o para el caso new, y en su lugar debería preferir los punteros inteligentes y / o el STL; esto se pregunta por pura curiosidad) ¿Por qué el estándar C ++ no permite este lanzamiento implícito, mientras que el estándar C lo hace?

wolfPack88
fuente
3
long *a = malloc(sizeof(int));¡Vaya, alguien olvidó cambiar solo un tipo!
Doval
44
@Doval: Eso todavía se soluciona fácilmente usando en su sizeof(*a)lugar.
wolfPack88
3
Creo que el punto de @ ratchetfreak es que la razón por la que C hace esta conversión implícita es porque mallocno puede devolver un puntero al tipo asignado. newsi C ++ devuelve un puntero al tipo asignado, por lo que el código C ++ escrito correctamente nunca tendrá ningún void *s para emitir.
Gort the Robot
3
Por otro lado, no es ningún otro tipo de puntero. Solo se necesitan aplicar punteros de datos.
Deduplicador
3
@ wolfPack88 tienes tu historia equivocada. C ++ tenía void, C no . Cuando se añadió la palabra clave / C idea, que cambiaron para adaptarse a las necesidades del C. Eso fue poco después de que los tipos de puntero comenzaron a ser revisados en absoluto . Vea si puede encontrar el folleto de descripción de K&R C en línea, o una copia antigua de un texto de programación en C como el C Primer de Waite Group . ANSI C estaba lleno, con características respaldadas o inspiradas en C ++, y K&R C era mucho más simple. Por lo tanto, es más correcto que C ++ extendiera C tal como existía en ese momento, y que la C que conoces se eliminó de C ++.
JDługosz

Respuestas:

39

Debido a que las conversiones de tipo implícitas generalmente no son seguras, y C ++ toma una postura más segura para escribir que C hace.

C generalmente permitirá conversiones implícitas, incluso si hay muchas posibilidades de que la conversión sea un error. Esto se debe a que C asume que el programador sabe exactamente lo que está haciendo, y si no, es el problema del programador, no el problema del compilador.

C ++ generalmente no permitirá cosas que podrían ser errores, y requerirá que explique explícitamente su intención con un tipo de conversión. Eso es porque C ++ está tratando de ser amigable con los programadores.

Puede preguntar por qué es amigable cuando en realidad requiere que escriba más.

Bueno, ya ves, cualquier línea de código dada, en cualquier programa, en cualquier lenguaje de programación, generalmente se leerá muchas más veces de lo que se escribirá (*). Entonces, la facilidad de lectura es mucho más importante que la facilidad de escritura. Y al leer, tener conversiones potencialmente inseguras se destacan por medio de conversiones de tipo explícito ayuda a comprender lo que está sucediendo y a tener un cierto nivel de certeza de que lo que está sucediendo es, de hecho, lo que estaba destinado a suceder.

Además, el inconveniente de tener que escribir el reparto explícito es trivial en comparación con el inconveniente de horas y horas de resolución de problemas para encontrar un error causado por una asignación errónea sobre la que podría haber sido advertido, pero nunca lo fue.

(*) Idealmente, se escribirá solo una vez, pero se leerá cada vez que alguien necesite revisarlo para determinar su idoneidad para la reutilización, y cada vez que haya una solución de problemas, y cada vez que alguien necesite agregar código cerca de él, y luego cada vez que hay una solución de problemas del código cercano, y así sucesivamente. Esto es cierto en todos los casos, excepto en los scripts de "escribir una vez, ejecutar y luego tirar", por lo que no es de extrañar que la mayoría de los lenguajes de scripting tengan una sintaxis que facilite la escritura sin tener en cuenta la facilidad de lectura. ¿Alguna vez pensaste que Perl es completamente incomprensible? No estas solo. Piense en tales idiomas como idiomas de "solo escritura".

Mike Nakis
fuente
C es esencialmente un paso por encima del código de máquina. Puedo perdonar a C por cosas como esta.
Qix
8
Los programas (cualquiera que sea el idioma) están destinados a ser leídos. Las operaciones explícitas se destacan.
Matthieu M.
10
Vale la pena señalar que las conversiones void*son más inseguras en C ++, porque con la forma en que se implementan ciertas características de OOP en C ++, el puntero al mismo objeto puede tener un valor diferente según el tipo de puntero.
hyde
@MatthieuM. muy cierto. Gracias por agregar eso, vale la pena ser parte de la respuesta.
Mike Nakis
1
@MatthieuM .: Ah, pero realmente no quieres tener que hacer todo explícitamente. La legibilidad no se mejora al tener más para leer. Aunque en este punto el equilibrio es claramente por ser explícito.
Deduplicador
28

Esto es lo que dice Stroustrup :

En C, puede convertir implícitamente un vacío * en un T *. Esto no es seguro

Luego muestra un ejemplo de cómo el vacío * puede ser peligroso y dice:

... En consecuencia, en C ++, para obtener una T * de un vacío *, necesita una conversión explícita. ...

Finalmente, él nota:

Uno de los usos más comunes de esta conversión insegura en C es asignar el resultado de malloc () a un puntero adecuado. Por ejemplo:

int * p = malloc (sizeof (int));

En C ++, use el nuevo operador typesafe:

int * p = nuevo int;

Él entra en muchos más detalles sobre esto en The Design and Evolution of C ++ .

Por lo tanto, la respuesta se reduce a: El diseñador de lenguaje cree que es un patrón inseguro, por lo que lo hizo ilegal y proporcionó formas alternativas de lograr para qué se usaba normalmente el patrón.

Gort the Robot
fuente
1
En relación con el mallocejemplo, vale la pena señalar que malloc(obviamente) no llamará a un constructor, por lo que si es un tipo de clase para el que está asignando memoria, ser capaz de emitir implícitamente al tipo de clase sería engañoso.
chbaker0
Esta es una muy buena respuesta, posiblemente mejor que la mía. Solo me disgusta un poco el enfoque de "argumento de la autoridad".
Mike Nakis
"new int" - wow, como novato en C ++ estaba teniendo problemas para pensar en una forma de agregar un tipo base al montón, y ni siquiera sabía que podías hacerlo. -1 uso para malloc.
Katana314
3
@MikeNakisFWIW, mi intención era complementar tu respuesta. En este caso, la pregunta era "¿por qué hicieron eso?", Así que pensé que escuchar del jefe de diseñadores estaba justificado.
Gort the Robot
11

En C, no es necesario lanzar un vacío * a ningún otro tipo de puntero, siempre se promueve de forma segura.

Siempre se promociona, sí, pero apenas con seguridad .

C ++ deshabilita este comportamiento precisamente porque intenta tener un sistema de tipos más seguro que C, y este comportamiento no es seguro.


Considere en general estos 3 enfoques para la conversión de tipos:

  1. obligar al usuario a escribir todo, para que todas las conversiones sean explícitas
  2. asuma que el usuario sabe lo que está haciendo y convierta cualquier tipo a otro
  3. Implemente un sistema de tipos completo con genéricos de tipo seguro (plantillas, nuevas expresiones), operadores de conversión explícitos definidos por el usuario, y fuerce conversiones explícitas solo de cosas que el compilador no puede ver son implícitamente seguras

Bueno, 1 es feo y un obstáculo práctico para hacer cualquier cosa, pero realmente podría usarse donde se necesita mucho cuidado. C optó por 2, que es más fácil de implementar, y C ++ por 3, que es más difícil de implementar pero más seguro.

Inútil
fuente
Fue un camino largo y difícil llegar a 3, con excepciones añadidas más tarde y plantillas aún más tarde.
JDługosz
Bueno, las plantillas no son realmente necesarias para un sistema de tipo seguro tan completo: los lenguajes basados ​​en Hindley-Milner se llevan bien sin ellos, sin conversiones implícitas y sin necesidad de escribir tipos explícitamente. (Por supuesto, estos lenguajes se basan en el borrado de tipo / recolección de basura / polimorfismo de tipo superior, cosas que C ++ evita)
Leftaroundabout
1

Por definición, un puntero vacío puede apuntar a cualquier cosa. Cualquier puntero se puede convertir en un puntero vacío y, por lo tanto, podrá volver a convertirlo llegando exactamente al mismo valor. Sin embargo, los punteros a otros tipos pueden tener restricciones, como restricciones de alineación. Por ejemplo, imagine una arquitectura donde los caracteres pueden ocupar cualquier dirección de memoria pero los números enteros deben comenzar incluso en los límites de la dirección. En ciertas arquitecturas, los punteros enteros incluso pueden contar 16, 32 o 64 bits a la vez, de modo que un char * podría tener un múltiplo del valor numérico de int * mientras apunta al mismo lugar en la memoria. En este caso, la conversión de un vacío * en realidad redondea los bits que no se pueden recuperar y, por lo tanto, no son reversibles.

En pocas palabras, el puntero vacío puede apuntar a cualquier cosa, incluidas las cosas que otros punteros pueden no ser capaces de señalar. Por lo tanto, la conversión al puntero vacío es segura pero no al revés.

roserez
fuente