Internacionalizace a lokalizace v C++

Ódy žlutého koně 2.

Ódy žlutého koně podruhé: Malá a velká písmena

Minule jsme si povídali o základních problémech s národním prostředím v programech v C++. Přitom jsme hovořili nejen o českém národním prostředí, ale o problémech s národním prostředím vůbec. Dozvěděli jsme se o třídě locale a o její spolupráci s objektovými datovými proudy. Dnes se podíváme na další úlohy, které je třeba v souvislosti s národním prostředím řešit.

Podobně jako v minulém dílu i zde musíme zdůraznit, že mnoho překladačů dosud tyto nástroje neimplementuje nebo je implementuje s chybami, proto nebuďte překvapeni, když vám zde uvedené příklady nebudou fungovat. Je však jisté, že v dohledné době se situace změní – nové verze vývojových nástrojů se budou muset přizpůsobit standardu. Koneckonců, novější programovací jazyky, jako je Java nebo C#, národní nastavení bez problémů zvládají, takže C++ nemůže zůstat výjimkou.

Neformátované proudy

Z předchozího dílu víme, že při formátovaném výstupu do proudu s použitím národního prostředí se znaky překládají do kódování, používaného v daném systému, pod Windows to znamená překlad do kódové stránky 1250. To ale platí i při neformátovaném výstupu. Podívejme se na následující příklad:

#include <fstream>
#include <iostream>
#include <locale>
#include <string>
#include <stdexcept>
using namespace std;
wchar_t cw[] = L"žluťoučký kůň příšerně úpěl ďábelské ódy";
int main()
{
try {
wofstream f("data.dta");
f.imbue(locale("Czech_Czech Republic.1250"));
if(!f) throw runtime_error("chyba proudu");
f.write(cw, sizeof(cw)/sizeof(wchar_t));
f.close();
}
catch(runtime_error &e)
{
cerr << e.what();
}
return 0;
}

Zde ukládáme data pomocí metody write(), určené pro neformátované operace – i ta ovšem převede kódování Unicode do zadané kódové stránky. Můžeme samozřejmě použít i kódovou stránku 852, pak dostaneme soubor v kódování Latin 2. Totéž platí i pro kódování ISO 8859-2 nebo pro češtinu pro MacIntosh.
Poznamenejme, že použijeme-li implicitní národní prostředí "C" (nebo vynecháme-li volání metody imbue() – výsledek bude týž), neuloží se do souboru data.dta nejspíš nic, neboť proud nebude umět převést české znaky z Unicode do odpovídající kódové stránky.

Jak zapsat Unicode do souboru
Ukazuje se tedy, že pro uložení textu v kódování Unicode se proudy moc nehodí, neboť dnešní implementace – alespoň pod Windows – neposkytují národní prostředí s kódováním, které by to umožňovalo. (Možná takové prostředí ve Windows existuje, nám se ho ale nepodařilo zatím zjistit.) To znamená, že chceme-li do souboru uložit opravdu znaky v kódování Unicode, musíme sáhnout po osvědčených funkcích pro neformátovaný zápis z jazyka C, jako je fwrite(). K uložení věty o žlutém koni do souboru data.dta můžeme použít následující program:

#include <cstdio>
using namespace std;
wchar_t cw[] = L"žluťoučký kůň příšerně úpěl ďábelské ódy";
int main() // Zapíše Unicode
{
FILE *f = fopen("data.dta", "wb");
fwrite(cw, sizeof(wchar_t), sizeof(cw)/sizeof(wchar_t), f);
fclose(f);
return 0;
}

I zde ovšem můžeme narazit na problémy. Pokud budeme číst uložená data na stejném počítači, jako jsme je zapisovali, bude vše v pořádku. Může se ovšem stát, že se na zdrojovém a cílovém počítači bude lišit způsob ukládání čísel do paměti.

Malý a velký endián
Běžně se používají dva zcela rovnocenné, ovšem navzájem nekompatibilní způsoby uložení dat v počítači, označované jako malý a velký endián (little, resp. big endian). Volba mezi nimi je zpravidla určena architekturou procesoru. Osobní počítače používají malý endián, ovšem například virtuální stroj jazyka Java používá velký endián, a to bez ohledu na to, na jakém počítači ho spouštíme.
Při malém endiánu se méně významné bajty ukládají na nižší adresy, při velkém endiánu se méně významné bajty ukládají na vyšší adresy. To znamená, že např. dvoubajtové číslo 0x1234 typu short se na počítači, používá malý endián, uloží v pořadí 0x34, 0x12, zatímco na počítači používajícím velký endián se uloží v pořadí 0x12, 0x34. Poznamenejme, že různé ukládání se týká nejen celých, ale i reálných čísel.
Typ wchar_t patří mezi celočíselné typy, takže se ho problémy s endiánem také týkají. Proto se dodržuje konvence, že na počátek souboru, obsahujícího Unicode, se zapisuje znak s hodnotou \uFEFF. Pokud zde při čtení najdeme znak \uFFFE, znamená to, že soubor byl zapsán v opačném endiánu, než jaký používá aktuální počítač, že tedy při čtení musíme prohodit pořadí bajtů v každém přečteném znaku.

Třída locale

S třídou locale jsme se zběžně seznámili již v minulém dílu. Než půjdeme dále, podíváme se na ni trochu blíže. Jednotlivé součásti národního prostředí jsou ve třídě locale zapouzdřeny do tzv. fazet, tedy tříd, odvozených od společného předka – třídy locale::facet. Než si ale o nich něco povíme, podíváme se na deklaraci třídy locale tak, jak ji najdeme ve standardu ISO 14882.

Deklarace třídy locale

namespace std {
class locale {
public:
// datové typy:
class facet;
class id;
typedef int category;
static const category // Skutečná implementace může užít
none = 0, // jiné hodnoty
collate = 0x010, ctype = 0x020,
monetary = 0x040, numeric = 0x080,
time = 0x100, messages = 0x200,
all = collate | ctype | monetary | numeric | time | messages;
// konstrukce, destrukce a kopírování:
locale() throw()
locale(const locale& other) throw()
explicit locale(const char* std_name);
locale(const locale& other, const char* std_name, category);
template <class Facet> locale(const locale& other, Facet* f);
locale(const locale& other, const locale& one, category);
~locale() throw(); // není virtuální
const locale& operator=(const locale& other) throw();
template <class Facet> locale combine(const locale& other);
// operace s instancí třídy locale:
basic_string<char> name() const;
bool operator==(const locale& other) const;
bool operator!=(const locale& other) const;
template <class charT, class Traits, class Allocator>
bool operator()(const basic_string<charT,Traits,Allocator>& s1,
const basic_string<charT,Traits,Allocator>& s2) const;
// globální objekty třídy locale:
static locale global(const locale&);
static const locale& classic();
};
}

Tato deklarace neukazuje soukromé složky, které implementují chování instancí. (Ty nejsou uvedeny ani ve standardu).
Konstanty typu category – kategorie – určují kategorie národního prostředí, podobné kategoriím známým z jazyka C.
Z konstruktorů zatím známe pouze jeden, jenž má jako parametr znakový řetězec se jménem národního prostředí. Význam kopírovacího konstruktoru je zřejmý. Konstruktor

template <class Facet> locale(const locale& other, Facet* f);

umožňuje vytvořit novou instanci třídy locale, jež bude kopií instance other, bude však obsahovat fazetu f. (V případě potřeby nahradí f jednu ze standardních fazet.) Konstruktor

locale(const locale& other, const locale& one, category cats);

slouží k podobnému účelu: Vytvoří kopii instance other, avšak fazety pro kategorie definované parametrem cats převezme z instance one.
Poznamenejme, že takto vytvořené instance třídy locale nemají jméno zavoláme-li pro takto vytvořenou instanci metodu name(), vrátí řetězec "*".
K porovnávání instancí třídy locale slouží operátory == a !=. Operátor == vrací true, je-li levý operand totožný s pravým, je-li jeden operand kopií druhého nebo mají-li oba stejné jméno.
Statická metoda locale::global() nastaví instanci, předanou jako parametr, jako globální národní prostředí, podobně jako funkce setlocale() z jazyka C. Zároveň vrátí instanci, představující předchozí národní prostředí.
Statická metoda locale::classic() vrátí odkaz na instanci národního prostředí, určeného řetězcem "C".
K dalším operacím s instancemi třídy locale se ještě dostaneme.

Třída locale jako kontejner na fazety
Už jsme si řekli, že jednotlivé součásti národního prostředí jsou zapouzdřeny do tzv. fazet. Deklarace třídy facet podle standardu jazyka vypadá takto:

namespace std {
class locale::facet {
protected:
explicit facet(size_t refs = 0);
virtual ~facet();
private:
facet(const facet&); // není definován
void operator=(const facet&); // není definován
};
}

Všimněte si, že tato třída neobsahuje žádné veřejně přístupné složky. Není tedy určena k bezprostřednímu použití, slouží jen jako společný předek pro třídy, které budou skutečně implementovat jednotlivé součásti národního prostředí. Kopírovací metody, tedy kopírovací konstruktor a přiřazovací operátor, jsou deklarovány jako soukromé a nejdou definovány. To zaručuje, že instance žádné z odvozených tříd nebude možné kopírovat.
Konstruktor má jeden celočíselný parametr, který určuje, zda se bude o zrušení fazety starat programátor nebo třída locale. Implicitní hodnota 0 znamená, že se o ni bude starat instance třídy locale, jež bude fazetu vlastnit.

Standardně obsahuje třída locale řadu fazet. Jsou definovány prostřednictvím šablon a třída locale obsahuje jejich instance jak pro široké, tak pro úzké znaky. Rozdělíme je do jednotlivých kategorií, v následujícím přehledu T znamená jak char, tak i wchar_t, zatímco C může být i jakýkoli jiný znakový typ.
1. V kategorii řazení znaků (collate) najdeme fazety collate<T> a collate_byname<T>.
2. V kategorii klasifikace typů (ctype) najdeme fazety ctype<T> a ctype_byname<T>.
3. V kategorii nástrojů pro práci s měnou (monetary) máme fazety moneypunct<T,International>, moneypunct_byname<T,International>, moneypunct_byname<T,true>, money_get<C>, money_get<C,InputIterator>, money_put<C> a money_put<C,OutputIterator>.
4. V kategorii formátování čísel (numeric) najdeme fazety numpunct<T>, numpunct_byname<T>, num_get<C,InputIterator> a num_put<C,OutputIterator>.
5. V kategorii formátování časových údajů (time) jsou fazety time_get<T,InputIterator>, time_get_byname<T,InputIterator>, time_put<T,OutputIterator>a time_put_byname<T,OutputIterator>.
6. Poslední je kategorie zpráv (messages). V ní najdeme fazety messages<T> a messages_byname<wchar_t>.
Fazeta s identifikátorem xxx_byname poskytuje tytéž nástroje jako fazeta s identifikátorem xxx, pouze má navíc konstruktor s parametrem, kterým je znakový řetězec obsahující jméno národního prostředí.
Parametr International u fazet pro práci s měnou je buď true nebo false a určuje, zda se budou používat mezinárodní zkratky pro měnu (CZK, CHF atd.). Parametry OutputIterator, resp. InputIterator umožňují zadat buffer, do něhož se bude výstup vypisovat.
Poznamenejme, že uživatel si může definovat i své vlastní fazety a při vytváření instance třídy locale pomocí konstruktoru je do ní přidat.
V tomto článku se ovšem zdaleka nedostaneme ke všem standardním fazetám.
Přístup k fazetám
Jednotlivé fazety daného národního prostředí můžeme používat prostřednictvím šablonové funkce

template<class Facet> const Facet& use_facet(const locale&);

Tato funkce vrací odkaz na požadovanou fazetu. Parametr Facet, udávající typ fazety, se v deklaraci objevuje pouze jako typ vracené hodnoty, proto musíme při volání zadat tento parametr šablony vždy uvádět. (Překladače, které explicitní specifikaci parametrů šablon u funkcí nepodporují, musí nabídnout způsob, jak to obejít). Parametrem funkce use_facet<>() je instance třídy locale, z níž chceme danou fazetu vzít. Podívejme se na příklad: V minulém dílu jsme při převodu mezi širokými a úzkými znaky používali fazetu ctype<wchar_t> z instance lokálního nastavení Kon příkazem

use_facet<ctype<wchar_t> >(Kon)

Protože výsledkem tohoto volání je odkaz na požadovanou fazetu, mohli jsme ihned volat její metodu narrow() příkazem

use_facet<ctype<wchar_t> >(Kon).narrow(
cw, cw+sizeof(cw)/sizeof(wchar_t), '*', cn);

Pokud požadovaná fazeta v dané instanci lokálního nastavení chybí, vyvolá tato metoda výjimku typu std::bad_cast. Chceme-li zjistit, zda určitá instance třídy locale obsahuje jistou fazetu, použijeme šablonovou funkci

template <class Facet> bool has_facet(const locale&) throw();

Také při použití této funkce musíme uvádět šablonový parametr, určující typ fazety. Parametrem této funkce pak je instance třídy locale.

Národní prostředí a datové proudy
Už víme, že instanci třídy locale můžeme připojit k datovému proudu, slouží k tomu metoda imbue(), definovaná ve třídě ios_base, která je společným předkem všech proudových tříd. Datový proud si uchovává kopii předané instance, to znamená, že původní instance může po volání metody imbue() zaniknout.
Chceme-li získat aktuální národní prostředí, které daný proud používá, zavoláme metodu getloc(), jež je také definována ve třídě ios_base.
Implicitně je u všech proudů nastaveno prostředí "C".

Znaky a jejich klasifikace

Vraťme se opět k problémům národních nastavení. Jednou z nejjednodušších úloh, na které můžeme narazit, je klasifikace znaků – zda jde o písmeno, číslici atd. – a převod mezi malými a velkými písmeny.

Převod mezi malými a velkými písmeny
Převod malých písmen na velká a naopak je snadný, omezíme-li se na anglickou abecedu v kódování ASCII. Následující verzi funkce toupper() napsal nejspíš každý, kdo se učil jazyk C nebo C++ :

char toupper(char c)
{
if((c >= 'a') && (c <= 'z'))
return c + 'A' - 'a';
else
return c;
}

Jakmile ovšem vstoupí do hry znaky národních abeced, situace se zkomplikuje. Můžeme narazit na následující nesnáze:
1. Národní prostředí nerozlišuje malá a velká písmena, to se týká např. hebrejštiny. Pak ovšem odpadají problémy s jejich vzájemným převodem.
2. Vztah mezi malými a velkými písmeny nemusí být vzájemně jednoznačný. Převodem malých písmen na velká a zpět tak může vzniknout dokonce jiné slovo. To se týká např. francouzštiny, v níž se mohou u velkých písmen vynechávat akcenty, u malých však nikoli. Např. slovo „ou“ (kde) se při převodu na velká písmena může změnit v OU a po převodu zpět dostaneme „ou“, což znamená „nebo“.
3. K malému písmenu neexistuje odpovídající velké písmeno. To se týká např. němčiny, kde se znak ß („ostré s“) při použití velkých písmen nahrazuje dvojicí SS. Například jméno jednoho ze nejznámějších německých matematiků, které se tradičně píše Gauß, bychom pomocí velkých písmen zapsali jako GAUSS. (Dvě s se také používají jako náhrada ß ve znakových sadách, které tento znak neobsahují, dnes se ovšem často používají místo ß i v běžné němčině.)
Standardní knihovna jazyka C++ umožňuje převod mezi malými a velkými písmeny, založený na vzájemně jednoznačné korespondenci, to znamená, že neřeší problémy 2 a 3. V češtině naštěstí na žádný z těchto problémů nenarazíme.
K převodu mezi malými a velkými písmeny použijeme fazetu ctype<wchat_t>, která v podstatě pokrývá vše, co umějí funkce ze standardní hlavičky ctype.h z jazyka C. Máme-li znakový řetězec

wchar_t cw[] = L"žluťoučký kůň příšerně úpěl ďábelské ódy";

můžeme ho převést na velká písmena příkazem

use_facet<ctype<wchar_t> >(L).toupper(cw, &cw[sizeof(cw)/sizeof(wchar_t)]);

Metoda toupper() má dva parametry, první z nich je ukazatel na první prvek převáděného úseku, druhý je ukazatel na poslední prvek tohoto úseku. Tato metoda vrací ukazatel na první prvek převedeného úseku.
Pokud odpovídající znak neexistuje, tj. pokud nějaký znak nelze převést na velké písmeno, ponechá tato funkce nezměněný původní znak.
Podívejme se na příklad. Vezmeme řetězec

wchar_t CW[100] = L"Gauß";;

a zkusíme ho v německém prostředí převést na velká písmena:

locale LG("german");
use_facet<ctype<wchar_t> >(LC)
.toupper(cw, &cw[sizeof(cw)/sizeof(wchar_t)]);

Výsledkem bude řetězec "GAUß". První znak nešlo převést, protože 'G' je již samo o sobě velké písmeno, poslední znak také ne, neboť k 'ß' odpovídající velké písmeno neexistuje. Oba znaky tedy zůstaly nezměněny.

Následující program ukazuje převod českého i německého textu. Poznamenejme, že na konzolu pod Windows se v německém prostředí používá kódová stránka 850, zatímco pro výstup do oken se používá kódová stránka 1252.

#include <locale>
#include <string>
#include <iostream>
using namespace std;
wchar_t cw[] = L"žluťoučký kůň příšerně úpěl ďábelské ódy";
wchar_t CW[100] = L"Gauß";;
int main(int argv, char *argc[])
{
locale LC("czech");
locale LG("German_Germany.1252");
// Měníme český text
wcout.imbue(locale("Czech_Czech Republic.852"));
wcout << cw << endl;
use_facet<ctype<wchar_t> >(LC)
.toupper(cw, &cw[sizeof(cw)/sizeof(wchar_t)]);
wcout << cw << endl;
// Měníme německý text
wcout.imbue(locale("German_Germany.850"));
wcout << CW << endl;
use_facet<ctype<wchar_t> >(LC).toupper(CW, &CW[4]);
wcout << CW << endl;
return 0;
}

Vedle metody, která zpracovává celý úsek řetězce, je ve fazetě ctype<> k dispozici také přetížená verze, která konvertuje jediný znak.
Pro převod na malá písmena je k dispozici analogická dvojice metod jménem tolower().

Klasifikace znaků
Fazeta ctype<> obsahuje metodu

bool is(mask m, charT c) const;

která umožňuje testovat, zda předaný znak vyhovuje určité masce, tj. zda patří do určité kategorie. Tyto masky jsou definovány jako bitové příznaky, Jde o hodnoty výčtového typu ctype_base::mask, pojmenované space, print, cntrl, upper, lower, digit, punct, xdigit, alpha, alnum, graph. Po řadě odpovídají bílým znakům, tisknutelným znakům, řídicím znakům, velkým písmenům, malým písmenům, interpunkčním znaménkům, šestnáctkovým číslicím, písmenům, alfanumerickým znakům a grafickým znakům.
Ve skutečnosti se ale funkce is() používá poměrně málo, v hlavičkovém souboru locale jsou totiž definovány šablony funkcí s povědomými jmény isspace(), isprint(), iscntrl(), isupper(), islower(), isalpha(), isdigit(), ispunct(), isxdigit(), isalnum() a isgraph(), které pokrývají převážnou většinu běžných situací. Deklarace všech těchto funkcí jsou stejné, proto postačí, když si uvedeme jen jednu z nich. V následující šabloně musí být charT znakový typ, tedy buď char nebo wchar_t:

template<class charT> bool isspace (charT c, const locale& loc);

Od stejnojmenných funkcí se standardní knihovny jazyka C se na první pohled liší vlastně jen tím, že mají navíc jako druhý parametr instanci třídy locale. (Na rozdíl od analogických funkcí ze standardní knihovny jazyka C se tedy neřídí globálně nastavenou hodnotou národního prostředí, ale používají národní prostředí, které jim předáme).

Upozornění
Ukázky kódu v tomto článku jsme, podobně jako v předchozím dílu, odladili ve Visual C++ .NET. V mnohých jiných překladačích pod Windows nejspíš nebudou fungovat nebo budou fungovat jen omezeně, neboť problematice národního prostředí věnují tvůrci překladačů pozornost až na jednom z posledních místech.
Pokud budete používat jiné prostředí než Windows, mohou se lišit řetězce, které určují jména národních prostředí. Ty je nutno hledat v dokumentaci (a zpravidla se v ní nehledají lehce).

Příště

Příště se podíváme na abecední řazení a formátování čísel.

Miroslav Virius