¿Qué hace que este uso de punteros sea impredecible?

108

Actualmente estoy aprendiendo consejos y mi profesor proporcionó este código como ejemplo:

//We cannot predict the behavior of this program!

#include <iostream>
using namespace std;

int main()
{
    char * s = "My String";
    char s2[] = {'a', 'b', 'c', '\0'};

    cout << s2 << endl;

    return 0;
}

Escribió en los comentarios que no podemos predecir el comportamiento del programa. Pero, ¿qué es exactamente lo que lo hace impredecible? No veo nada malo en ello.

trungnt
fuente
2
¿Está seguro de haber reproducido correctamente el código del profesor? Si bien es posible argumentar formalmente que este programa podría producir un comportamiento "impredecible", no tiene sentido hacerlo. Y dudo que algún profesor use algo tan esotérico para ilustrar "impredecible" a los estudiantes.
2015
1
@Lightness Races in Orbit: los compiladores pueden "aceptar" código mal formado después de emitir los mensajes de diagnóstico requeridos. Pero la especificación del lenguaje no define el comportamiento del código. Es decir, debido al error en la inicialización del sprograma, si es aceptado por algún compilador, formalmente tiene un comportamiento impredecible.
2015
2
@TheParamagneticCroissant: No. La inicialización está mal formada en los tiempos modernos.
Lightness Races in Orbit
2
@The Paramagnetic Croissant: Como dije anteriormente, el lenguaje no requiere un código mal formado para "fallar al compilar". Los compiladores simplemente deben emitir un diagnóstico. Después de eso, se les permite continuar y compilar el código "con éxito". Sin embargo, el comportamiento de dicho código no está definido por la especificación del idioma.
2015
2
Me encantaría saber cuál fue la respuesta que le dio su profesor.
Daniël W. Crompton

Respuestas:

125

El comportamiento del programa es inexistente porque está mal formado.

char* s = "My String";

Esto es ilegal Antes de 2011, había estado en desuso durante 12 años.

La línea correcta es:

const char* s = "My String";

Aparte de eso, el programa está bien. ¡Tu profesor debería beber menos whisky!

Carreras de ligereza en órbita
fuente
10
con -pedantic lo hace: main.cpp: 6: 16: advertencia: ISO C ++ prohíbe convertir una constante de cadena en 'char *' [-Wpedantic]
marcinj
17
@black: No, el hecho de que la conversión sea ilegal hace que el programa esté mal formado. Fue desaprobado en el pasado . Ya no estamos en el pasado.
Lightness Races in Orbit
17
(Lo cual es una tontería porque ese fue el propósito de la depreciación de 12 años)
Lightness Races in Orbit
17
@black: El comportamiento de un programa mal formado no está "perfectamente definido".
Lightness Races in Orbit
11
Independientemente, la pregunta es sobre C ++, no sobre alguna versión particular de GCC.
Lightness Races in Orbit
81

La respuesta es: depende del estándar C ++ con el que compile. Todo el código está perfectamente formado en todos los estándares ‡ con la excepción de esta línea:

char * s = "My String";

Ahora, el literal de cadena tiene tipo const char[10]y estamos tratando de inicializar un puntero que no sea constante. Para todos los demás tipos distintos de la charfamilia de cadenas literales, dicha inicialización siempre fue ilegal. Por ejemplo:

const int arr[] = {1};
int *p = arr; // nope!

Sin embargo, en pre-C ++ 11, para cadenas literales, hubo una excepción en §4.2 / 2:

Un literal de cadena (2.13.4) que no es un literal de cadena ancha se puede convertir a un rvalue de tipo " puntero a char "; [...]. En cualquier caso, el resultado es un puntero al primer elemento de la matriz. Esta conversión se considera solo cuando hay un tipo de destino de puntero apropiado explícito, y no cuando existe una necesidad general de convertir de un lvalue a un rvalue. [Nota: esta conversión está obsoleta . Ver Anexo D. ]

Entonces, en C ++ 03, el código está perfectamente bien (aunque en desuso) y tiene un comportamiento claro y predecible.

En C ++ 11, ese bloque no existe; no existe tal excepción para los literales de cadena convertidos a char*, por lo que el código está tan mal formado como el int*ejemplo que acabo de proporcionar. El compilador está obligado a emitir un diagnóstico, e idealmente en casos como este que son claras violaciones del sistema de tipos C ++, esperaríamos que un buen compilador no solo se ajuste a este respecto (por ejemplo, emitiendo una advertencia) sino que falle. total.

Idealmente, el código no debería compilarse, pero lo hace tanto en gcc como en clang (supongo que porque probablemente hay mucho código que se rompería con poca ganancia, a pesar de que este tipo de agujero del sistema ha estado en desuso durante más de una década). El código está mal formado y, por lo tanto, no tiene sentido razonar sobre cuál podría ser el comportamiento del código. Pero considerando este caso específico y la historia de que se permitió previamente, no creo que sea un tramo irrazonable interpretar el código resultante como si fuera un implícito const_cast, algo como:

const int arr[] = {1};
int *p = const_cast<int*>(arr); // OK, technically

Con eso, el resto del programa está perfectamente bien, ya que nunca volverás a tocar s. Leer un constobjeto creado a través de un no constpuntero está perfectamente bien. Escribir un constobjeto creado a través de dicho puntero es un comportamiento indefinido:

std::cout << *p; // fine, prints 1
*p = 5;          // will compile, but undefined behavior, which
                 // certainly qualifies as "unpredictable"

Como no hay modificaciones en sninguna parte de su código, el programa está bien en C ++ 03, debería fallar al compilar en C ++ 11 pero lo hace de todos modos, y dado que los compiladores lo permiten, todavía no hay un comportamiento indefinido en él † . Teniendo en cuenta que los compiladores siguen interpretando [incorrectamente] las reglas de C ++ 03, no veo nada que pueda conducir a un comportamiento "impredecible". Sin sembargo, escriba a y todas las apuestas están canceladas. Tanto en C ++ 03 como en C ++ 11.


† Aunque, nuevamente, por definición, el código mal formado no genera expectativas de comportamiento razonable
‡ Excepto que no, vea la respuesta de Matt McNabb

Barry
fuente
Creo que aquí el profesor pretende que "impredecible" signifique que no se puede usar el estándar para predecir lo que hará un compilador con un código mal formado (más allá de emitir un diagnóstico). Sí, podría tratarlo como C ++ 03 dice que debe tratarse, y (a riesgo de la falacia de "ningún escocés verdadero") el sentido común nos permite predecir con cierta confianza que esto es lo único que un compilador-escritor sensato alguna vez elegirá si el código se compila en absoluto. Por otra parte, podría tratarlo como si tuviera que invertir el literal de cadena antes de convertirlo en no constante. A C ++ estándar no le importa.
Steve Jessop
2
@SteveJessop No compro esa interpretación. Este no es un comportamiento indefinido ni de la categoría de código mal formado que las etiquetas estándar como no requieren diagnóstico. Es una simple violación del sistema de tipos que debería ser muy predecible (compila y hace cosas normales en C ++ 03, falla al compilar en C ++ 11). Realmente no puede usar errores del compilador (o licencias artísticas) para sugerir que el código es impredecible; de ​​lo contrario, todo el código sería tautológicamente impredecible.
Barry
No estoy hablando de errores del compilador, estoy hablando de si el estándar define o no el comportamiento (si lo hay) del código. Sospecho que el profesor está haciendo lo mismo, y "impredecible" es solo una forma torpe de decir que el estándar actual no define el comportamiento. De todos modos, eso me parece más probable que que el profesor crea incorrectamente que este es un programa bien formado con un comportamiento indefinido.
Steve Jessop
1
No, no lo hace. El estándar no define el comportamiento de programas mal formados.
Steve Jessop
1
@supercat: es un punto justo, pero no creo que sea la razón principal. Creo que la razón principal por la que el estándar no especifica el comportamiento de los programas mal formados es para que los compiladores puedan admitir extensiones del lenguaje agregando una sintaxis que no está bien formada (como lo hace Objective C). Permitir que la implementación haga una limpieza total después de una compilación fallida es solo una ventaja :-)
Steve Jessop
20

Otras respuestas han cubierto que este programa está mal formado en C ++ 11 debido a la asignación de una const charmatriz a char *.

Sin embargo, el programa también estaba mal diseñado antes de C ++ 11.

Las operator<<sobrecargas están en <ostream>. El requisito de iostreamincluir ostreamse agregó en C ++ 11.

Históricamente, la mayoría de las implementaciones habían iostreamincluido de ostreamtodos modos, quizás para facilitar la implementación o quizás para proporcionar una mejor QoI.

Pero sería adecuado iostreamdefinir solo la ostreamclase sin definir las operator<<sobrecargas.

MM
fuente
13

Lo único ligeramente incorrecto que veo con este programa es que se supone que no debes asignar un literal de cadena a un charpuntero mutable , aunque esto a menudo se acepta como una extensión del compilador.

De lo contrario, este programa me parece bien definido:

  • Las reglas que dictan cómo las matrices de caracteres se convierten en punteros de caracteres cuando se pasan como parámetros (como con cout << s2) están bien definidas.
  • La matriz tiene terminación en nulo, que es una condición para operator<<con a char*(o a const char*).
  • #include <iostream>incluye <ostream>, que a su vez define operator<<(ostream&, const char*), por lo que todo parece estar en su lugar.
zneak
fuente
12

No se puede predecir el comportamiento del compilador, por las razones mencionadas anteriormente. ( Debería fallar al compilar, pero puede que no).

Si la compilación tiene éxito, entonces el comportamiento está bien definido. Sin duda, puede predecir el comportamiento del programa.

Si no se compila, no hay programa. En un lenguaje compilado, el programa es el ejecutable, no el código fuente. Si no tiene un ejecutable, no tiene un programa y no puede hablar sobre el comportamiento de algo que no existe.

Entonces diría que la declaración de su profesor es incorrecta. No se puede predecir el comportamiento del compilador cuando se enfrenta a este código, pero eso es distinto del comportamiento del programa . Entonces, si va a picar liendres, será mejor que se asegure de tener razón. O, por supuesto, es posible que lo haya citado mal y el error esté en la traducción de lo que dijo.

Graham
fuente
10

Como han señalado otros, el código es ilegítimo en C ++ 11, aunque era válido en versiones anteriores. En consecuencia, se requiere un compilador para C ++ 11 para emitir al menos un diagnóstico, pero el comportamiento del compilador o del resto del sistema de compilación no se especifica más allá de eso. Nada en el Estándar prohibiría a un compilador salir abruptamente en respuesta a un error, dejando un archivo de objeto parcialmente escrito que un enlazador podría pensar que es válido, produciendo un ejecutable roto.

Aunque un buen compilador siempre debe asegurarse antes de salir de que cualquier archivo objeto que se espera que haya producido será válido, inexistente o reconocible como inválido, tales cuestiones quedan fuera de la jurisdicción del Estándar. Si bien históricamente ha habido (y aún puede haber) algunas plataformas en las que una compilación fallida puede dar como resultado archivos ejecutables de apariencia legítima que se bloquean de manera arbitraria cuando se cargan (y he tenido que trabajar con sistemas donde los errores de enlace a menudo tenían ese comportamiento) , No diría que las consecuencias de los errores de sintaxis son generalmente impredecibles. En un buen sistema, un intento de compilación generalmente producirá un ejecutable con el mejor esfuerzo del compilador en la generación de código, o no producirá un ejecutable en absoluto. Algunos sistemas dejarán atrás el antiguo ejecutable después de una compilación fallida,

Mi preferencia personal sería que los sistemas basados ​​en disco cambien el nombre del archivo de salida, para tener en cuenta las raras ocasiones en las que ese ejecutable sería útil y al mismo tiempo evitar la confusión que puede resultar de creer erróneamente que uno está ejecutando un código nuevo, y para la programación integrada sistemas para permitir que un programador especifique para cada proyecto un programa que debe cargarse si un ejecutable válido no está disponible con el nombre normal [idealmente algo que indique con seguridad la falta de un programa utilizable]. Un conjunto de herramientas de sistemas embebidos generalmente no tendría forma de saber qué debería hacer tal programa, pero en muchos casos alguien que escriba código "real" para un sistema tendrá acceso a algún código de prueba de hardware que podría adaptarse fácilmente a la propósito. No sé si he visto el comportamiento de cambio de nombre, sin embargo,

Super gato
fuente