Internacionalizace a lokalizace v C++
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.
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.
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".
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ě se podíváme na abecední řazení a formátování čísel.
Miroslav Virius