4.3.5. Cadenas y rasgos de caracteres

El programa Find.cpp anterior en este capítulo nos lleva a hacernos la pregunta obvia: ¿por que la comparación sensible a mayúsculas/minúsculas no es parte de la clase estándar string? La respuesta nos brinda un interesante transfondo sobre la verdadera naturaleza de los objetos string en C++.

Considere qué significa para un carácter tener "mayúscula/minúscula". El Hebreo escrito, el Farsi, y el Kanji no usan el concepto de "mayúscula/minúscula", con lo que para esas lenguas esta idea carece de significado. Esto daria a entender que si existiera una manera de designar algunos lenguages como "todo mayúsculas" o "todo minúsculas", podriamos diseñar una solución generalizada. Sin embargo, algunos leguajes que emplean el concepto de "mayúscula/minúscula", tambien cambian el significado de caracteres particulares con acentos diacríticos, por ejemplo la cedilla del Español, el circumflexo en Francés y la diéresis en Alemán. Por esta razón, cualquier codificación sensible a mayúsculas que intenta ser comprensiva acaba siendo una pesadilla en su uso.

Aunque tratamos habitualmente el string de C++ como una clase, esto no es del todo cierto. El tipo string es una especialización de algo más general, la plantilla basic_string< >. Observe como está declarada string en el fichero de cabecera de C++ estándar.

typedef basic_string<char> string;

Para comprender la naturaleza de la clase string, mire la plantilla basic_string< >

template<class charT, class traits = char_traits<charT>, class allocator = allocator<charT> > class basic_string;

En el Capítulo 5, examinamos las plantillas con gran detalle (mucho más que en el Capítulo 16 del volúmen 1). Por ahora nótese que el tipo string es creada cuando instanciamos la plantilla basic_string con char. Dentro de la declaración plantilla basic_string< > la línea:

class traits = char_traits<charT<,

nos dice que el comportamiento de la clase hecha a partir de basic_string< > es defineida por una clase basada en la plantilla char_traits< >. Así, la plantilla basic_string< > produce clases orientadas a string que manipulan otros tipos que char (caracteres anchos, por ejemplo). Para hacer esto, la plantilla char_traits< > controla el contenido y el comportamiento de la ordenación de una variedad de conjuntos de caracteres usando las funciones de comparación eq() (equal), ne() (not equal), y lt() (less than). Las funciones de comparación de basic_string< > confian en esto.

Es por esto por lo que la clase string no incluye funciones miembro sensibles a mayúsculas/minúsculas: eso no esta en la descripción de su trabajo. Para cambiar la forma en que la clase string trata la comparación de caracteres, tiene que suministrar una plantilla char_traits< > diferente ya que define el comportamiento individual de las funciones miembro de comparación carácteres.

Puede usar esta información para hacer un nuevo tipo de string que ignora las mayúsculas/minúsculas. Primero, definiremos una nueva plantilla no sensible a mayúsculas/minúsculas de char_traits< > que hereda de una plantilla existente. Luego, sobrescribiremos sólo los miembros que necesitamos cambiar para hacer la comparación carácter por carácter. (Además de los tres miembros de comparación léxica mencionados antes, daremos una nueva implementación para laspara las funciones de char_traits find() y compare()). Finalmente, haremos un typedef de una nueva clase basada en basic_string, pero usando nuestra plantilla insensible a mayúsculas/minúsculas, ichar_traits, como segundo argumento:

//: C03:ichar_traits.h
// Creating your own character traits.
#ifndef ICHAR_TRAITS_H
#define ICHAR_TRAITS_H
#include <cassert>
#include <cctype>
#include <cmath>
#include <cstddef>
#include <ostream>
#include <string>
using std::allocator;
using std::basic_string;
using std::char_traits;
using std::ostream;
using std::size_t;
using std::string;
using std::toupper;
using std::tolower;

struct ichar_traits : char_traits<char> {
  // We'll only change character-by-
  // character comparison functions
  static bool eq(char c1st, char c2nd) {
    return toupper(c1st) == toupper(c2nd);
  }
  static bool ne(char c1st, char c2nd) {
    return !eq(c1st, c2nd);
  }
  static bool lt(char c1st, char c2nd) {
    return toupper(c1st) < toupper(c2nd);
  }
  static int
  compare(const char* str1, const char* str2, size_t n) {
    for(size_t i = 0; i < n; ++i) {
      if(str1 == 0)
        return -1;
      else if(str2 == 0)
        return 1;
      else if(tolower(*str1) < tolower(*str2))
        return -1;
      else if(tolower(*str1) > tolower(*str2))
        return 1;
      assert(tolower(*str1) == tolower(*str2));
      ++str1; ++str2; // Compare the other chars
    }
    return 0;
  }
  static const char*
  find(const char* s1, size_t n, char c) {
    while(n-- > 0)
      if(toupper(*s1) == toupper(c))
        return s1;
      else
        ++s1;
    return 0;
  }
};

typedef basic_string<char, ichar_traits> istring;

inline ostream& operator<<(ostream& os, const istring& s) {
  return os << string(s.c_str(), s.length());
}
#endif // ICHAR_TRAITS_H ///:~

Listado 4.29. C03/ichar_traits.h


Proporcionamos un typedef llamado istring ya que nuestra clase actuará como un string ordinario en todas sus formas, excepto que realizará todas las comparaciones sin respetar las mayúsculas/minúsculas. Por conveniencia, damos un operador sobrecargado operator<<() para que pueda imprimir los istring. Aque hay un ejemplo:

//: C03:ICompare.cpp
#include <cassert>
#include <iostream>
#include "ichar_traits.h"
using namespace std;

int main() {
  // The same letters except for case:
  istring first = "tHis";
  istring second = "ThIS";
  cout << first << endl;
  cout << second << endl;
  assert(first.compare(second) == 0);
  assert(first.find('h') == 1);
  assert(first.find('I') == 2);
  assert(first.find('x') == string::npos);
} ///:~

Listado 4.30. C03/ICompare.cpp


Este es solo un ejemplo de prueba. Para hacer istring completamente equivalente a un string, deberiamos haber creado las otras funciones necesarias para soportar el nuevo tipo istring.

La cabecera <string> provee de un string ancho [8] gracias al siguiente typedef:

typedef basic_string<wchar_t> wstring;

El soporte para string ancho se revela tambien en los streams anchos (wostream en lugar de ostream, tambien definido en <iostream>) y en la especialización de wchar_t de los char_traits en la libreria estándar le da la posibilidad de hacer una version de carácter ancho de ichar_traits

//: C03:iwchar_traits.h {-g++}
// Creating your own wide-character traits.
#ifndef IWCHAR_TRAITS_H
#define IWCHAR_TRAITS_H
#include <cassert>
#include <cmath>
#include <cstddef>
#include <cwctype>
#include <ostream>
#include <string>

using std::allocator;
using std::basic_string;
using std::char_traits;
using std::size_t;
using std::towlower;
using std::towupper;
using std::wostream;
using std::wstring;

struct iwchar_traits : char_traits<wchar_t> {
  // We'll only change character-by-
  // character comparison functions
  static bool eq(wchar_t c1st, wchar_t c2nd) {
    return towupper(c1st) == towupper(c2nd);
  }
  static bool ne(wchar_t c1st, wchar_t c2nd) {
    return towupper(c1st) != towupper(c2nd);
  }
  static bool lt(wchar_t c1st, wchar_t c2nd) {
    return towupper(c1st) < towupper(c2nd);
  }
  static int compare(
    const wchar_t* str1, const wchar_t* str2, size_t n) {
    for(size_t i = 0; i < n; i++) {
      if(str1 == 0)
        return -1;
      else if(str2 == 0)
        return 1;
      else if(towlower(*str1) < towlower(*str2))
        return -1;
      else if(towlower(*str1) > towlower(*str2))
        return 1;
      assert(towlower(*str1) == towlower(*str2));
      ++str1; ++str2; // Compare the other wchar_ts
    }
    return 0;
  }
  static const wchar_t*
  find(const wchar_t* s1, size_t n, wchar_t c) {
    while(n-- > 0)
      if(towupper(*s1) == towupper(c))
        return s1;
      else
        ++s1;
    return 0;
  }
};

typedef basic_string<wchar_t, iwchar_traits> iwstring;

inline wostream& operator<<(wostream& os,
  const iwstring& s) {
  return os << wstring(s.c_str(), s.length());
}
#endif // IWCHAR_TRAITS_H  ///:~

Listado 4.31. C03/iwchar_traits.h


Como puede ver, esto es principalmente un ejercicio de poner 'w' en el lugar adecuado del código fuente. El programa de prueba podria ser asi:

//: C03:IWCompare.cpp {-g++}
#include <cassert>
#include <iostream>
#include "iwchar_traits.h"
using namespace std;

int main() {
  // The same letters except for case:
  iwstring wfirst = L"tHis";
  iwstring wsecond = L"ThIS";
  wcout << wfirst << endl;
  wcout << wsecond << endl;
  assert(wfirst.compare(wsecond) == 0);
  assert(wfirst.find('h') == 1);
  assert(wfirst.find('I') == 2);
  assert(wfirst.find('x') == wstring::npos);
} ///:~

Listado 4.32. C03/IWCompare.cpp


Desgraciadamente, todavia algunos compiladores siguen sin ofrecer un soporte robusto para caracteres anchos.



[8] (N.del T.) Se refiere a string amplio puesto que esta formado por caracteres anchos wchar_t que deben soportar la codificación mas grande que soporte el compilador. Casi siempre esta codificación es Unicode, por lo que casi siempre el ancho de wchar_t es 2 bytes