¿Necesito convertir a un carácter sin firmar antes de llamar a toupper(), tolower(), et al.?
Hace un tiempo, alguien con gran reputación aquí en Stack Overflow escribió en un comentario que es necesario emitir un char
argumento unsigned char
antes de llamar std::toupper
a y std::tolower
(y funciones similares).
Por otro lado, Bjarne Stroustrup no menciona la necesidad de hacerlo en el Lenguaje de Programación C++ . Él solo usa toupper
como
string name = "Niels Stroustrup";
void m3() {
string s = name.substr(6,10); // s = "Stroustr up"
name.replace(0,5,"nicholas"); // name becomes "nicholas Stroustrup"
name[0] = toupper(name[0]); // name becomes "Nicholas Stroustrup"
}
(Citado de dicho libro, 4ª edición.)
La referencia dice que la entrada debe poder representarse como unsigned char
. Para mí, esto parece que vale para todos char
los tiempos char
y unsigned char
tiene el mismo tamaño.
Entonces, ¿este reparto es innecesario o Stroustrup fue descuidado?
Editar: El manual de libstdc++ menciona que el carácter de entrada debe ser del conjunto de caracteres fuente básico , pero no lo convierte. Supongo que esto está cubierto por la respuesta de @Keith Thompson, todos tienen una representación positiva como signed char
y unsigned char
?
toupper
Sí, es necesario convertir el argumento a unsigned char
para evitar el riesgo de un comportamiento indefinido.
Los tipos char
, signed char
y unsigned char
son tres tipos distintos. char
tiene el mismo rango y representación que o . (El formato normal suele estar firmado y puede representar valores en el rango -128..+127).signed char
unsigned char
char
La toupper
función toma un int
argumento y devuelve un int
resultado. Citando la norma C, sección 7.4 párrafo 1:
En todos los casos el argumento es un
int
, cuyo valor será representable como ununsigned char
o será igual al valor de la macroEOF
. Si el argumento tiene cualquier otro valor, el comportamiento no está definido.
(C++ incorpora la mayor parte de la biblioteca estándar de C y difiere su definición al estándar C).
El []
operador de indexación on std::string
devuelve una referencia a char
. Si simple char
es un tipo con signo y si el valor de name[0]
resulta ser negativo, entonces la expresión
toupper(name[0])
tiene un comportamiento indefinido.
El lenguaje garantiza que, incluso si el formato simple char
está firmado, todos los miembros del conjunto de caracteres básico tienen valores no negativos, por lo que dada la inicialización
string name = "Niels Stroustrup";
el programa no corre el riesgo de tener un comportamiento indefinido. Pero sí, en general, un char
valor pasado a toupper
(o a cualquiera de las funciones declaradas en <cctype>
/ <ctype.h>
) debe convertirse a unsigned char
, de modo que la conversión implícita a int
no produzca un valor negativo y provoque un comportamiento indefinido.
Las <ctype.h>
funciones se implementan comúnmente mediante una tabla de búsqueda. Algo como:
// assume plain char is signed
char c = -2;
c = toupper(c); // undefined behavior
puede indexar fuera de los límites de esa tabla.
Tenga en cuenta que la conversión a unsigned
:
char c = -2;
c = toupper((unsigned)c); // undefined behavior
no evita el problema. Si int
es de 32 bits, al convertir el char
valor -2
a unsigned
se obtiene 4294967294
. Luego, esto se convierte implícitamente a int
(el tipo de parámetro), lo que probablemente produce -2
.
toupper
se puede implementar para que se comporte de manera sensata con valores negativos (aceptando todos los valores desde CHAR_MIN
hasta UCHAR_MAX
), pero no es necesario hacerlo. Además, las funciones <ctype.h>
deben aceptar un argumento con el valor EOF
, que normalmente es -1
.
El estándar C++ realiza ajustes en algunas funciones de la biblioteca estándar de C. Por ejemplo, strchr
varias funciones más se reemplazan por versiones sobrecargadas que imponen const
la corrección. No existen tales ajustes para las funciones declaradas en <cctype>
.
La referencia se refiere a que el valor se puede representar como unsigned char
, no a que seaunsigned char
. Es decir, el comportamiento no está definido si el valor real no está entre 0 y UCHAR_MAX
(normalmente 255). (O EOF
, que es básicamente la razón por la que se necesita un int
en lugar de un char
).
En C, toupper
(y muchas otras funciones) toman int
s aunque es de esperar que tomen char
s. Además, char
está firmado en algunas plataformas y sin firmar en otras.
El consejo de transmitir unsigned char
antes de llamar toupper
es correcto para C. No creo que sea necesario en C++, siempre que lo pase y No puedo encontrar nada específico sobre si es necesario en C++.int
esté dentro del rango.
Si desea evitar el problema, utilice lo toupper
definido en<locale>
. Es una plantilla y acepta cualquier tipo de carácter aceptable. También tienes que pasarle un std::locale
. Si no tiene idea de qué configuración regional elegir, utilice std::locale("")
, que se supone que es la configuración regional preferida del usuario:
#include <algorithm>
#include <iostream>
#include <iterator>
#include <locale>
#include <string>
int main()
{
std::string name("Bjarne Stroustrup");
std::string uppercase;
std::locale loc("");
std::transform(name.begin(), name.end(), std::back_inserter(uppercase),
[&loc](char c) { return std::toupper(c, loc); });
std::cout << name << '\n' << uppercase << '\n';
return 0;
}
Lamentablemente, Stroustrup fue descuidado :-(
Y sí, los códigos de letras latinas no deben ser negativos (y no se requiere conversión)...
Algunas implementaciones funcionan correctamente sin convertir caracteres sin firmar...
Según cierta experiencia, puede costar varios horas para encontrar la causa de la falla de segmento de dicho toupper (cuando se sabe que hay una falla de segmento)...
Y también hay isupper, islower, etc.