C++ STL fun -- re: std::locale és toupper

Erre a kérdésre támadt kedvem összeállítani egy C++-ízű megoldást. Íme:


/* toupper.cpp
 *
 * COMMON="-ansi -pedantic -Wall -Wextra -o toupper toupper.cpp"
 *
 * If you saved this file with UTF-8 encoding:
 *   g++ $COMMON -finput-charset=UTF-8
 *
 * If you saved this file with latin2 encoding:
 *   g++ $COMMON -finput-charset=latin2
 *
 * Run the binary:
 *   ./toupper
 */

#define GPP_BUG_35353 1 /* http://gcc.gnu.org/bugzilla/show_bug.cgi?id=35353 */

#if GPP_BUG_35353
#  include <clocale>  /* ::setlocale() */
#endif

#include <functional> /* std::bind1st() */
#include <string>     /* std::wstring */
#include <locale>     /* std::locale */
#include <algorithm>  /* std::for_each() */
#include <iostream>   /* std::wcout */


/* Function object composition. Takes (= copies) two function objects that
 * conform to C++03 20.3 Function objects [lib.function.objects] paragraph 5,
 * and returns a function object that implements their composition, and also
 * conforms to said paragraph.
 */
template <class Op1, class Op2>
class Compose : public std::unary_function<typename Op1::argument_type,
                                           typename Op2::result_type>
{
  public:
    Compose(const Op1& op1, const Op2& op2) : op1(op1), op2(op2) {}

    typename Op2::result_type operator()(const typename Op1::argument_type &in)
        const { return op2(op1(in)); }

  private:
    const Op1 op1;
    const Op2 op2;
};

template <class Op1, class Op2> Compose<Op1, Op2>
compose(const Op1& op1, const Op2& op2)
{
  return Compose<Op1, Op2>(op1, op2);
}


int
main()
{
#if GPP_BUG_35353
  ::setlocale(LC_ALL, "");
#endif

  /* Phase 1.
   *
   * Dependent on the "-finput-charset" g++ option, wide characters of this
   * string that fall outside the basic source character set are mapped to
   * universal characters correctly. C++03 2.1 Phases of translation
   * [lex.phases] paragraph 1.
   *
   * A universal-character-name is then encoded into the execution character
   * set, see C++03 2.13.2 Character literals [lex.ccon] paragraph 5. See also
   * the "-fwide-exec-charset" g++ option -- the default should be OK (UCS-2
   * (UTF-16) or UCS-4 (UTF-32), based on the size of "wchar_t").
   */
  const std::wstring str(L"árvíztűrő tükörfúrógép");

  /* Get the user's preferred locale from the environment. See C++03 22.1.1.2
   * locale constructors and destructor [lib.locale.cons] paragraph 8, and
   * C++03 22.2.8 Program-defined facets [lib.facets.examples] paragraph 5.
   */
  const std::locale cur_loc("");

#if !GPP_BUG_35353
  /* Ensure wide cout formats / converts according to user preference. This is
   * a mess. See C++03 27.4.2.3 ios_base locale functions
   * [lib.ios.base.locales] and C++03 22.2.8 Program-defined facets
   * [lib.facets.examples].
   */
  std::wcout.imbue(cur_loc);
#endif

  /* Thus this prints str to stdout, according to the users LC_CTYPE. */
  std::wcout << str << std::endl;


  /* Phase 2.
   *
   * This typedef basically means "LC_CTYPE for wchar_t", for any given given
   * locale. See C++03 22.2.1.1 Class template ctype [lib.locale.ctype].
   */
  typedef std::ctype<wchar_t> lc_ctype;

  /* Capture the ctype category of the user's preferred locale. See C++03
   * 22.1.1 Class locale [lib.locale] paragraph 4.
   */
  const lc_ctype& lc_ctype_cur = std::use_facet<lc_ctype>(cur_loc);

  /* "Program C++ in Haskell".
   *
   * See C++03 20.3 Function objects [lib.function.objects] and C++03 25.1.1
   * For each [lib.alg.foreach].
   *
   * lc_ctype::toupper() is a member function. For any given "lc_ctype" locale
   * category object, it implements "toupper". It takes a "wchar_t".
   *
   * std::mem_fun() as used here takes a pointer to a member function, and
   * returns a function object whose operator() takes two parameters: (1)
   * pointer to the object on which we'd like to invoke the member function,
   * (2) the argument to pass to the member function.
   *
   * std::bind1st() takes a function object whose operator() takes two
   * parameters, and binds the first. The returned function object only takes
   * one parameter (the unbound, second one).
   *
   * We create the following function objects here:
   *
   * (a) A functor that runs the lc_ctype::toupper() member function on an
   *     "lc_ctype" instance, and passes a "wchar_t" to it. This functor
   *     upper-cases a wide character in some locale.
   *
   * (b) A functor that binds functor (a) to the users preferred locale's
   *     LC_CTYPE category, so that functor (b) only takes a "wchar_t". This
   *     functor implements the traditional "toupper" in the user's preferred
   *     locale.
   *
   * (c) A functor thar runs the std::wostream::put() member function on an
   *     "std::wostream" instance, and passes a "wchar_t" to it. This functor
   *     prints a wide character to some wide output stream.
   *
   * (d) A functor that binds functor (c) to "std::wcout", so that functor (d)
   *     only takes a "wchar_t". This functor prints a wide character to
   *     standard output.
   *
   * (e) A functor that is the composition of (b) and (d): it upper-cases a
   *     wide character in the user's preferred locale, then prints it to
   *     standard output.
   *
   * Then we run functor (e) on all elements of "str".
   */
  std::for_each(str.begin(), str.end(), compose(
      std::bind1st(std::mem_fun(&lc_ctype::toupper), &lc_ctype_cur),
      std::bind1st(std::mem_fun(&std::wostream::put), &std::wcout)
  ));
  std::wcout << std::endl;
}

Hozzászólások

A fájl UTF-8-ban mentve, majd clang-2.8 és g++ 4.2.1 eredménye FreeBSD-n (ugyenzt kapom, ha UTF-8-at használó locale-lal futtatom UTF-8-as xterm-ben)

$ ./toupper.clang
terminate called without an active exception
Abort trap
$ ./toupper.gcc
terminate called after throwing an instance of 'std::runtime_error'
what(): locale::facet::_S_create_c_locale name not valid
Abort trap
$ locale
LANG=hu_HU.ISO8859-2
LC_CTYPE="hu_HU.ISO8859-2"
LC_COLLATE="hu_HU.ISO8859-2"
LC_TIME="hu_HU.ISO8859-2"
LC_NUMERIC="hu_HU.ISO8859-2"
LC_MONETARY="hu_HU.ISO8859-2"
LC_MESSAGES="hu_HU.ISO8859-2"
LC_ALL=
$

Könnyen reprodukálható nálam is, ha valamelyik LC_XXX változóban érvénytelen értéket adok meg. A "locale" parancs (paraméterek nélkül) csak annyit jelez vissza, hogy a felhasználó milyen beállításokat szeretne érvényesíteni; arról nem szól, hogy ebből mi elégíthető ki.

Ez a sor hajít el egy std::runtime_error-t (a [lib.locale.cons] p7 által dokumentáltan):


  const std::locale cur_loc("");

Biztosat persze csak úgy fogsz tudni, ha engedélyezed a core dump írását, és belenézel gdb-vel (... és ha a forrást -g3-mal fordítod).

Az a parancs, hogy "locale -k", nem jelez hibát? Pl. LC_MESSAGES hiányára nem panaszkodik?

... Arra akarok kilyukadni, hogy a hu_HU.ISO8859-2 nincs 100%-osan telepítve. Egy "rendes" programnak természetesen az ilyen hibákat is kezelnie kell, de most ezzel nem foglalkoztam. Pl. a ::setlocale() visszatérési értékét sem ellenőrzöm.

Az LC_MESSAGES legegyszerűbb ellenőrzése (a fenti LANG beállítás mellett):


touch /tmp/dummy
rm -i /tmp/dummy

Ha az "rm" kérdésére "i"-vel (és nem "y"-nal) válaszolsz, letörli a file-t?

Hm, kösz a tippet. A locale -k FreeBSD-n paramétert vár. Ellenben az rm -es kérdésed talált. Már csak az a furcsa, hogy vagy 15 éve én csináltam meg a FreeBSD hu locale-ját, és tudom, hogy akkor még volt benne yesstr és nostr :-) Persze az is lehet, hogy simán az rm nincs felkészítve a locale kezelésére. Erre utal ez is:

$ locale -k yesstr
yesstr="igen"
$ locale -k nostr
nostr="nem"
$

Már csak arra nem emlékszem, mi a neve a rövid yesstr/nostr változóknak. (Pár locale-t lekérdezve, elég kevésben van ez a kettő implementálva. Szégyen.)

Kosz, valoban. Es mit ad isten, most megneztem ezt az egeszet 9.0-s FreeBSD-n. Immar hu_HU.UTF-8 a locale, a magyaritas jo is, yesstr/nostr/yesexpr/noexpr mind gyonyoru; g++ ugyanaz, de immar 3.0-as clang/llvm. Es a hibauzenet is ugyanaz, szoval valami mast hianyolunk abbol a locale-bol, nem az igen es nem szavakat.