Го­воря о тек­сте, боль­шинс­тво прог­раммис­тов C++ дума­ют о мас­сивах кодов сим­волов и кодиров­ке, которой эти коды соот­ветс­тву­ют. Наибо­лее опыт­ные раз­работ­чики вооб­ще не мыс­лят понятие тек­ста без ука­зания кодиров­ки, наиме­нее опыт­ные прос­то счи­тают мас­сив бай­тов с кодами сим­волов дан­ностью и интер­пре­тиру­ют в поняти­ях кодиров­ки опе­раци­онной сис­темы. Фун­дамен­таль­ная раз­ница меж­ду эти­ми дву­мя под­ходами не толь­ко в опы­те раз­работ­чика, но и в том, что не думать о кодиров­ке нам­ного про­ще. Пора рас­смот­реть спо­соб, как не заботить­ся о хра­нении кодиров­ки, переко­диров­ке тек­ста, получать сво­бод­ный дос­туп к сим­волам и при этом видеть безоши­боч­ное пред­став­ление тек­ста вне зависи­мос­ти от того, кто и где смот­рит на стро­ку тек­ста: в Китае ли, в США или на остро­ве Мадагас­кар.
 

8 бит и все-все-все…

Нач­нем с глав­ного. Соз­датели язы­ка си были минима­лис­тами. По сей день в стан­дарте C/C++ не пре­дус­мотре­но типа «байт». Вмес­то это­го типа исполь­зует­ся тип char. Char озна­чает character, ины­ми сло­вами — сим­вол. Соот­ветс­твен­но, говоря в С/С++ о типе char, мы под­разуме­ваем «байт», и наобо­рот. Вот тут и начина­ется самое инте­рес­ное. Дело в том, что мак­сималь­но воз­можное чис­ло сим­волов, кодиру­емых 8 битами, рав­но 256, и это при том, что на сегод­няшний день в таб­лице Unicode нас­читыва­ются сот­ни тысяч сим­волов. Хит­рые соз­датели ASCII-кодов сра­зу же зарезер­вирова­ли пер­вые 128 кодов под стан­дар­тные сим­волы, которы­ми сме­ло мож­но закоди­ровать прак­тичес­ки все в англо­языч­ном мире, оста­вив нам лишь полови­ну бай­та под свои нуж­ды, а точ­нее лишь один сво­бод­ный стар­ший бит.

В резуль­тате в пер­вые годы ста­нов­ления информа­тики все пытались ужать­ся в эти оставши­еся «отри­цатель­ные» чис­ла от –128 до –1. Каж­дый набор кодов стан­дарти­зиро­вал­ся под опре­делен­ным име­нем и с это­го момен­та име­новал­ся кодиров­кой. В какой‑то момент кодиро­вок ста­ло боль­ше, чем сим­волов в бай­те, и все они были несов­мести­мы меж­ду собой в той час­ти, что выходи­ла за пре­делы пер­вых 128 ASCII-сим­волов. В резуль­тате, если не уга­дать с кодиров­кой, все, что не явля­ет собой набор сим­волов пер­вой необ­ходимос­ти для аме­рикан­ско­го сооб­щес­тва, будет отоб­ражено в виде так называ­емых кра­козябр, сим­волов, как пра­вило, вооб­ще нечита­емых.

Ма­ло того, для одних и тех же алфа­витов раз­ные сис­темы вво­дили кодиров­ки, совер­шенно рас­согла­сован­ные меж­ду собой, даже если это две сис­темы за авторс­твом одной ком­пании. Так, для кирил­лицы в MS DOS исполь­зовались кодиров­ки 855 и 866, а для Windows уже 1251, все для той же кирил­лицы в Mac OS исполь­зует­ся уже своя кодиров­ка, особ­няком от них сто­ят KOI8 и KOI7, есть даже ISO 8859-5, и все будут трак­товать одни и те же наборы char совер­шенно раз­ными сим­волами. Мало того, что было невоз­можно при обра­бот­ке раз­личных байт‑сим­волов поль­зовать­ся сра­зу нес­коль­кими кодиров­ками, нап­ример при перево­де с рус­ско­го на немец­кий с умла­ута­ми, вдо­бавок сами сим­волы в некото­рых алфа­витах ну никак не хотели помещать­ся в оставлен­ные для них 128 позиций. В резуль­тате в интерна­циональ­ных прог­раммах сим­волы мог­ли интер­пре­тиро­вать­ся в раз­ных кодиров­ках даже в сосед­них стро­ках, при­ходи­лось запоми­нать, какая стро­ка в какой кодиров­ке, что неиз­бежно вело к ошиб­кам отоб­ражения тек­ста, от забав­ных до сов­сем не смеш­ных.

Пос­тавь себе на вир­туаль­ную машину любую дру­гую опе­раци­онную сис­тему с дру­гой кодиров­кой по умол­чанию, нежели на тво­ей хос­товой сис­теме, нап­ример Windows c кодиров­кой 1251, если у тебя Linux с UTF-8 по умол­чанию, и наобо­рот. Поп­робуй написать код с выводом стро­ки кирил­лицей в std::cout, который без изме­нения кода будет собирать­ся и работать под обе­ими сис­темами оди­нако­во. Сог­ласись, интерна­циона­лиза­ция кросс‑плат­формен­ного кода не такая прос­тая задача.

 

Пришествие Юникода

За­дум­ка Юни­кода была прос­та. Каж­дому сим­волу раз и нав­сегда прис­ваивает­ся один код на веки веч­ные, это стан­дарти­зует­ся в оче­ред­ной вер­сии спе­цифи­кации таб­лицы сим­волов Юни­кода, и код сим­вола уже не огра­ничен одним бай­том. Велико­леп­ная задум­ка во всем, кро­ме одно­го: в язы­ки прог­рамми­рова­ния C/C++ и не толь­ко в них сим­вол char раз и нав­сегда ассо­цииро­вал­ся с бай­том. Пов­сюду в коде под­разуме­вал­ся sizeof(char), рав­ный еди­нице. Стро­ки тек­ста же были обыч­ными пос­ледова­тель­нос­тями этих самых char, закан­чива­ющи­мися сим­волом с нулевым кодом. В защиту соз­дателей язы­ка си, Рит­чи и Кер­нигана, сле­дует ска­зать, что в те далекие 70-е годы ник­то и подумать не мог, что для кодиро­вания сим­вола понадо­бит­ся так мно­го кодов, ведь для кодиро­вания сим­волов печат­ной машин­ки впол­не хва­тало и бай­та. Как бы то ни было, основное зло было сот­ворено, любое изме­нение типа char при­вело бы к потере сов­мести­мос­ти с уже написан­ным кодом.

Ра­зум­ным решени­ем ста­ло вве­дение нового типа «широко­го сим­вола» wchar_t и дуб­лирова­ние всех стан­дар­тных фун­кций язы­ка си для работы с новыми, «широки­ми» стро­ками. Кон­тей­нер стан­дар­тной биб­лиоте­ки C++ string так­же обрел «широко­го» соб­рата wstring. Все рады и счас­тли­вы, если бы не одно «но»: все уже при­вык­ли писать код на осно­ве бай­товых строк, а пре­фикс L перед стро­ковым литера­лом не добав­лял энту­зиаз­ма раз­работ­чикам на C/C++. Люди пред­почита­ли не исполь­зовать сим­волы за пре­дела­ми ASCII и сми­рить­ся с огра­ничен­ностью латини­цы, чем писать неп­ривыч­ные конс­трук­ции, несов­мести­мые с уже написан­ным кодом, работав­шим с типом char.

Ос­ложня­ло ситу­ацию то, что wchar_t не име­ет стан­дарти­зиро­ван­ного раз­мера: нап­ример, в сов­ремен­ных GCC-ком­пилято­рах g++ он 4 бай­та, в Visual C++ — 2 бай­та, а раз­работ­чики Android NDK уре­зали его до одно­го бай­та и сде­лали неот­личимым от char. Получи­лось так себе решение, которое работа­ет далеко не вез­де. С одной сто­роны, 4-бай­тный wchar_t наибо­лее бли­зок к прав­де, так как по стан­дарту один wchar_t дол­жен соот­ветс­тво­вать одно­му сим­волу Юни­кода, с дру­гой сто­роны, ник­то не гаран­тиру­ет, что будет имен­но 4 бай­та в коде, исполь­зующем wchar_t.

Аль­тер­натив­ным решени­ем ста­ла одно­бай­товая кодиров­ка UTF-8, которая мало того, что сов­мести­ма с ASCII (стар­ший бит, рав­ный нулю, отве­чает за одно­бай­товые сим­волы), так еще и поз­воля­ет кодиро­вать вплоть до 4-бай­тового целого, то есть свы­ше 2 мил­лиар­дов сим­волов. Пла­та, прав­да, доволь­но сущес­твен­ная, сим­волы получа­ются раз­лично­го раз­мера, и что­бы, нап­ример, заменить латин­ский сим­вол R на рус­ский сим­вол Я, пот­ребу­ется пол­ностью перес­тро­ить всю стро­ку, что зна­читель­но дороже обыч­ной замены кода в слу­чае 4-бай­тового wchar_t.

Та­ким обра­зом, любая активная работа с сим­волами стро­ки в UTF-8 может пос­тавить крест на идее исполь­зовать дан­ную кодиров­ку. Тем не менее кодиров­ка доволь­но ком­пак­тно ужи­мает текст, содер­жит защиту от оши­бок чте­ния и, глав­ное, интерна­циональ­на: любой человек в любой точ­ке мира уви­дит одни и те же сим­волы из таб­лицы Юни­кода, если будет читать стро­ку, закоди­рован­ную в UTF-8. Конеч­но, за исклю­чени­ем слу­чая, ког­да пыта­ется интер­пре­тиро­вать эту стро­ку в дру­гой кодиров­ке, все пом­нят «кра­козяб­ры» при попыт­ке открыть кирил­лицу в UTF-8 как текст в кодиров­ке по умол­чанию в Windows 1251.

 

Устройство однобайтного Юникода

Ус­тро­ена кодиров­ка UTF-8 весь­ма занят­но. Вот основные прин­ципы:

  1. Сим­вол кодиру­ется пос­ледова­тель­ностью бай­тов, в каж­дом бай­те лидиру­ющие биты кодиру­ют позицию бай­та в пос­ледова­тель­нос­ти, а для пер­вого бай­та еще и дли­ну пос­ледова­тель­нос­ти. Нап­ример, так выг­лядит в UTF-8 сим­вол Я:

    [1101 0000] [1010 1111]
  2. Бай­ты пос­ледова­тель­нос­ти, начиная со вто­рого, всег­да начина­ются с битов 10, соот­ветс­твен­но, пер­вый байт пос­ледова­тель­нос­ти кода каж­дого сим­вола начинать­ся с 10 не может. На этом стро­ится основная про­вер­ка кор­рек­тнос­ти декоди­рова­ния кода сим­вола из UTF-8.

  3. Пер­вый байт может быть единс­твен­ным, тог­да лидиру­ющий бит равен 0 и сим­вол соот­ветс­тву­ет коду ASCII, пос­коль­ку для кодиро­вания оста­ется 7 млад­ших бит.

  4. Ес­ли сим­вол не ASCII, то пер­вые биты содер­жат столь­ко еди­ниц, сколь­ко байт в пос­ледова­тель­нос­ти, вклю­чая лидиру­ющий байт, пос­ле чего идет 0 как окон­чание пос­ледова­тель­нос­ти еди­ниц и потом уже зна­чащие биты пер­вого бай­та. Как вид­но из при­веден­ного при­мера, кодиро­вание сим­вола Я занима­ет 2 бай­та, это мож­но рас­познать по стар­шим двум битам пер­вого бай­та пос­ледова­тель­нос­ти.

  5. Все зна­чащие биты скле­ивают­ся в еди­ную пос­ледова­тель­ность бит и уже интер­пре­тиру­ются как чис­ло. Нап­ример, для любого сим­вола, кодиру­емо­го дву­мя бай­тами, зна­чащие биты я условно помечу сим­волом x:

    [110x xxxx] [10xx xxxx]

При склей­ке, как вид­но, мож­но получить чис­ло, кодиру­емое 11 битами, то есть вплоть до 0x7FF сим­вола таб­лицы Юни­кода. Это­го впол­не хва­тает для сим­волов кирил­лицы, рас­положен­ной в пре­делах от 0x400 до 0x530. При склей­ке сим­вола Я из при­мера получит­ся код:

1 0000 10 1111

Как раз 0x42F — код сим­вола Я в таб­лице сим­волов Юни­кода.

Дру­гими сло­вами, если не работать с сим­волами в стро­ке, заменяя их дру­гими сим­волами из таб­лицы Юни­кода, то мож­но исполь­зовать кодиров­ку UTF-8, она надеж­на, ком­пак­тна и сов­мести­ма с типом char в том пла­не, что эле­мен­ты строк сов­пада­ют по раз­меру с бай­том, но не обя­затель­но явля­ются при этом сим­волами.

Собс­твен­но, имен­но эффектив­ностью и популяр­ностью кодиров­ки UTF-8 и обус­ловле­но насиль­ствен­ное вве­дение одно­бай­тового wchar_t в Android NDK, раз­работ­чики при­зыва­ют исполь­зовать UTF-8, а «широкие» стро­ки не приз­нают как жиз­неспо­соб­ный вид. С дру­гой сто­роны, Google не так дав­но отри­цал даже исклю­чения в C++, одна­ко весь мир не перес­поришь, будь ты хоть триж­ды Google, и обра­бот­ку исклю­чений приш­лось под­держать. Что каса­ется wchar_t сим­волов с раз­мером в один байт, то мно­жес­тво биб­лиотек уже при­вык­ло к мытарс­твам с типом wchar_t и дуб­лиру­ют «широкий» фун­кци­онал обра­бот­кой обыч­ных бай­товых строк.

info

UTF (Unicode Transformation Format) — по сути бай­товое пред­став­ление тек­ста, исполь­зующее коды сим­волов из таб­лицы Юни­кода, запако­ван­ные в бай­товый мас­сив сог­ласно стан­дарти­зиро­ван­ным пра­вилам. Наибо­лее популяр­ны UTF-8 и UTF-16, которые пред­став­ляют сим­волы эле­мен­тами по 8 бит и по 16 бит соот­ветс­твен­но.

В обо­их слу­чаях сим­вол совер­шенно необя­затель­но занима­ет ров­но 8 или 16 бит, нап­ример, в UTF-16 исполь­зуют­ся сур­рогат­ные пары, по сути пары 16-бит­ных зна­чений, исполь­зуемых вмес­те. В резуль­тате зна­чащих битов ста­новит­ся мень­ше (20 в слу­чае сур­рогат­ной пары), чем битов в груп­пе пред­став­ляющих сим­вол, но воз­можнос­ти кодиро­вать сим­волы начина­ют пре­вышать огра­ниче­ния в 256 или 65 536 зна­чений, и мож­но закоди­ровать любой сим­вол из таб­лицы Юни­кода.

Вы­год­но отли­чающий­ся от соб­рать­ев UTF-32 менее популя­рен, вви­ду избы­точ­ности пред­став­ления дан­ных, что кри­тич­но при боль­шом объ­еме тек­ста.

 

Пишем по-русски в коде

Бе­ды и дис­кри­мина­ция по язы­ково­му приз­наку начина­ются, ког­да мы пыта­емся исполь­зовать в коде стро­ку на язы­ке, отличном от ASCII. Так, Visual Studio под Windows соз­дает все фай­лы в кодиров­ке фай­ловой сис­темы по умол­чанию (1251), и при попыт­ке открыть код со стро­ками по‑рус­ски в том же Linux с кодиров­кой по умол­чанию UTF-8 получим кучу непонят­ных сим­волов вмес­то исходно­го тек­ста.

Си­туацию час­тично спа­сает пересох­ранение исходни­ков в кодиров­ке UTF-8 с обя­затель­ным сим­волом BOM, без него Visual Studio начина­ет интер­пре­тиро­вать «широкие» стро­ки с кирил­лицей весь­ма сво­еоб­разно. Одна­ко, ука­зав BOM (Byte Order Mark — мет­ка поряд­ка бай­тов) кодиров­ки UTF-8  — сим­вол, кодиру­емый тре­мя бай­тами 0xEF, 0xBB и 0xBF, мы получа­ем узна­вание кодиров­ки UTF-8 в любой сис­теме.

BOM — стан­дар­тный заголо­воч­ный набор бай­тов, нуж­ный для рас­позна­вания кодиров­ке тек­ста в Юни­коде, для каж­дой из кодиро­вок UTF он выг­лядит по‑раз­ному.

Не стес­няй­ся исполь­зовать род­ной язык в прог­рамме. Даже если тебе при­дет­ся локали­зовы­вать ее в дру­гие стра­ны, механиз­мы интерна­циона­лиза­ции помогут прев­ратить любую стро­ку на одном язы­ке в любую стро­ку на дру­гом. Разуме­ется, это в слу­чае, если про­дукт раз­рабаты­вает­ся в рус­ско­языч­ном сег­менте.

Ста­рай­ся исполь­зовать «широкие» стро­ки как для стро­ковых кон­стант, так и для хра­нения и обра­бот­ки про­межу­точ­ных тек­сто­вых зна­чений. Эффектив­ная замена сим­волов, а так­же сов­падение количес­тва эле­мен­тов в стро­ке с количес­твом сим­волов дорого­го сто­ит. Да, до сих пор не все биб­лиоте­ки научи­лись работать с «широки­ми» сим­волами, даже в Boost попада­ется целый ряд биб­лиотек, где под­дер­жка широких строк сде­лана неб­режно, но ситу­ация исправ­ляет­ся, во мно­гом бла­года­ря раз­работ­чикам, пишущим ошиб­ки в тре­кер биб­лиоте­ки, не стес­няй­ся и ты фик­сировать ошиб­ки на сай­те раз­работ­чика биб­лиоте­ки.

Пи­сать кон­стан­ты и перемен­ные, а так­же наз­вания фун­кций кирил­лицей все же не нуж­но. Одно дело — выводить кон­стан­ты на род­ном язы­ке, сов­сем дру­гое — писать код, пос­тоян­но перек­лючая рас­клад­ку. Не самый луч­ший вари­ант.

 

Различаем тип «байты» и тип «текст»

Глав­ное, что нуж­но уметь и иметь в виду, — что тип «текст» в кор­не отли­чает­ся от типа «набор бай­тов». Если мы говорим о стро­ке сооб­щения, то это текст, а если о тек­сто­вом фай­ле в некото­рой кодиров­ке, то это набор бай­тов, который мож­но вычитать как текст. Если по сети нам при­ходят тек­сто­вые дан­ные, то они при­ходят к нам имен­но бай­тами, вмес­те с ука­зани­ем кодиров­ки, как из этих бай­тов получить текст.

Ес­ли пос­мотреть на Python 3 в срав­нении с Python 2, то третья вер­сия совер­шила по‑нас­тояще­му серь­езный ска­чок в раз­витии, раз­делив эти два понятия. Край­не рекомен­дую даже опыт­ному C/C++ раз­работ­чику порабо­тать нем­ного в Python 3, что­бы ощу­тить всю глу­бину, с которой про­изош­ло раз­деление тек­ста и бай­тов на уров­не язы­ка в Python. Фак­тичес­ки текст в Python 3 отде­лен от понятия кодиров­ки, что для раз­работ­чика C/C++ зву­чит край­не неп­ривыч­но, стро­ки в Python 3 отоб­ража­ются оди­нако­во в любой точ­ке мира, и если мы хотим работать с пред­став­лени­ем этой стро­ки в какой‑либо кодиров­ке, то при­дет­ся пре­обра­зовать текст в набор бай­тов, с ука­зани­ем кодиров­ки. При этом внут­реннее пред­став­ление объ­екта типа str, по сути, не так важ­но, как понима­ние, что внут­реннее пред­став­ление сох­ранено в Юни­коде и готово к пре­обра­зова­нию в любую кодиров­ку, но уже в виде набора бай­тов типа bytes.

В C/C++ подоб­ный механизм нам меша­ет ввес­ти отсутс­твие такой рос­коши, как потеря обратной сов­мести­мос­ти, которую поз­волил себе Python 3 отно­ситель­но вто­рой вер­сии. Одно лишь раз­деление типа char на ана­лог wchar_t и byte в одной из сле­дующих редак­ций стан­дарта при­ведет к кол­лапсу язы­ка и потере сов­мести­мос­ти с непомер­ным количес­твом уже написан­ного кода на С/С++. Точ­нее, все­го, на чем ты сей­час работа­ешь.

 

Веселые перекодировки

Итак, исходная проб­лема оста­лась нерешен­ной. У нас по‑преж­нему есть одно­бай­товые кодиров­ки, как UTF-8, так и ста­рые и недоб­рые одно­бай­товые кодиров­ки вро­де кодиров­ки Windows 1251. С дру­гой сто­роны, мы зада­ем стро­ковые кон­стан­ты широки­ми стро­ками и обра­баты­ваем текст через wchar_t — «широкие» сим­волы.

Здесь нам на помощь при­дет механизм переко­диро­вок. Ведь, зная кодиров­ку набора бай­тов, мы всег­да смо­жем пре­обра­зовать его в набор сим­волов wchar_t и обратно. Не спе­ши толь­ко самос­тоятель­но соз­давать свою биб­лиоте­ку переко­диров­ки, я понимаю, что коды сим­волов любой кодиров­ки сей­час мож­но най­ти за минуту, как и всю таб­лицу кодов Юни­кода пос­ледней редак­ции. Одна­ко биб­лиотек переко­диров­ки дос­таточ­но и без это­го. Есть кросс‑плат­формен­ная биб­лиоте­ка libiconv, под лицен­зией LGPL, самая популяр­ная на сегод­няшний день для кросс‑плат­формен­ной раз­работ­ки. Переко­диров­ка сво­дит­ся к нес­коль­ким инс­трук­циям:

iconv_t conv = iconv_open("UTF-8","CP1251");
iconv(conv, &src_ptr, &src_len, &dst_ptr, &dst_len);
iconv_close(conv);

Со­ответс­твен­но, сна­чала соз­дание обра­бот­чика переко­диров­ки из одной кодиров­ки в дру­гую, затем сама опе­рация переко­диров­ки одно­го набора бай­тов в дру­гой (даже если один из наборов бай­тов на самом деле бай­ты мас­сива wchar_t), пос­ле чего обя­затель­ное зак­рытие соз­данно­го обра­бот­чика переко­диров­ки.

Есть так­же и более амби­циоз­ная биб­лиоте­ка ICU, которая пре­дос­тавля­ет как C++ интерфейс для работы с переко­диров­кой, так и спе­циаль­ный тип icu::UnicodeString для хра­нения непос­редс­твен­но тек­ста в пред­став­лении Юни­кода. Биб­лиоте­ка ICU так­же явля­ется кросс‑плат­формен­ной, и вари­антов ее исполь­зования пре­дос­тавля­ется на порядок боль­ше. При­ятно, что биб­лиоте­ка сама заботит­ся о соз­дании, кеширо­вании и при­мене­нии обра­бот­чиков для переко­диров­ки, если исполь­зовать C++ API биб­лиоте­ки.

Нап­ример, что­бы соз­дать стро­ку в Юни­коде, пред­лага­ется исполь­зовать обыч­ный конс­трук­тор клас­са icu::UnicodeString:

icu::UnicodeString text(source_bytes, source_encoding);

Та­ким обра­зом, пред­лага­ется пол­ностью отка­зать­ся от типа wchar_t. Проб­лема, одна­ко, в том, что внут­реннее пред­став­ление Юни­кода для такой стро­ки уста­нов­лено в два бай­та, что вле­чет за собой проб­лему в слу­чае, ког­да код за эти два бай­та выходит. Кро­ме того, интерфейс icu::UnicodeString пол­ностью несов­местим со стан­дар­тным wstring, одна­ко исполь­зование ICU — хороший вари­ант для С++ раз­работ­чика.

Кро­ме того, есть пара стан­дар­тных фун­кций mbstowcs и wcstombs. В общем и целом при пра­виль­но задан­ной локали они, соот­ветс­твен­но, пре­обра­зуют (муль­ти-) бай­товую стро­ку в «широкую» и наобо­рот. Рас­шифро­выва­ются сок­ращения mbs и wcs как Multi-Byte String и Wide Character String соот­ветс­твен­но. Кста­ти, боль­шинс­тво при­выч­ных раз­работ­чику на язы­ке си фун­кций работы со стро­ками дуб­лиру­ются имен­но фун­кци­ями, в которых в наз­вании str замене­но на wcs, нап­ример wcslen вмес­то strlen или wcscpy вмес­то strcpy.

Нель­зя не вспом­нить и о Windows-раз­работ­ке. Счас­тли­вых обла­дате­лей WinAPI ждет оче­ред­ная пара фун­кций с кучей парамет­ров: WideCharToMultiByte и MultiByteToWideChar. Дела­ют эти фун­кции ров­но то, что говорят их наз­вания. Ука­зыва­ем кодиров­ку, парамет­ры вход­ного и выход­ного мас­сива и фла­ги и получа­ем резуль­тат. Нес­мотря на то что фун­кции эти внеш­не страш­нень­кие, работу свою дела­ют быс­тро и эффектив­но. Прав­да, не всег­да точ­но: могут попытать­ся пре­обра­зовать сим­вол в похожий, поэто­му осто­рож­нее с фла­гами, которые переда­ются вто­рым парамет­ром в фун­кцию, луч­ше ука­зать WC_NO_BEST_FIT_CHARS.

При­мер исполь­зования:

WideCharToMultiByte( CP_UTF8,
WC_NO_BEST_FIT_CHARS,
pszWideSource, nWideLength,
pszByteSource, nByteLength,
NULL, NULL );

Ра­зуме­ется, этот код не перено­сим на любую плат­форму, кро­ме Windows, поэто­му край­не рекомен­дую поль­зовать­ся кросс‑плат­формен­ными биб­лиоте­ками ICU4C или libiconv.

info

На­ибо­лее популяр­ная биб­лиоте­ка — имен­но libiconv, одна­ко в ней исполь­зуют­ся исклю­читель­но парамет­ры char*. Это не дол­жно пугать, в любом слу­чае мас­сив чисел любой бит­ности — это все­го лишь набор бай­тов. Сле­дует, одна­ко, пом­нить про нап­равле­ние дву­бай­товых и более чисел. То есть в каком поряд­ке в бай­товом мас­сиве пред­став­лены бай­ты — ком­понен­ты чис­ла. Раз­лича­ют Big-endian и Little-endian соот­ветс­твен­но.

Об­щепри­нятый порядок пред­став­ления чисел в подав­ляющем боль­шинс­тве машин — Little-endian: сна­чала идет млад­ший байт, а в кон­це стар­ший байт чис­ла. Big-endian зна­ком тем, кто работа­ет с про­токо­лами переда­чи дан­ных по сети, где чис­ла при­нято переда­вать начиная со стар­шего бай­та (час­то содер­жащего слу­жеб­ную информа­цию) и кон­чая млад­шим. Сле­дует быть акку­рат­ным и пом­нить, что UTF-16, UTF-16BE и UTF-16LE — не одно и то же.

 

Класс текста

Да­вай теперь акку­мули­руем получен­ные зна­ния и решим исходную задачу: нам нуж­но соз­дать сущ­ность, по сути класс, ини­циали­зиру­емый стро­кой, либо «широкой», либо бай­товой, с ука­зани­ем кодиров­ки, и пре­дос­тавля­ющий интерфейс при­выч­ного кон­тей­нера стро­ки std::string, с воз­можностью обра­щения к эле­мен­там‑сим­волам, изме­няя их, уда­ляя, пре­обра­зуя экзем­пляр тек­ста в стро­ке как «широкой», так и бай­товой с ука­зани­ем кодиров­ки. В общем, нам нуж­но зна­читель­но упростить работу с Юни­кодом, с одной сто­роны, и получить сов­мести­мость с преж­де написан­ным кодом, с дру­гой сто­роны.

Класс тек­ста, таким обра­зом, получит сле­дующие конс­трук­торы:

text(char const* byte_string, char const* encoding);
text(wchar_t const* wide_string);

Сто­ит перег­рузить так­же от std::string и std::wstring вари­антов, а так­же от ите­рато­ров начала и кон­ца кон­тей­нера‑источни­ка.

Дос­туп к эле­мен­ту, оче­вид­но, дол­жен быть открыт, но в качес­тве резуль­тата нель­зя исполь­зовать бай­товый char или плат­формо­зави­симый wchar_t, мы дол­жны исполь­зовать абс­трак­цию над целочис­ленным кодом в таб­лице Юни­кода: symbol.

symbol& operator [] (int index);
symbol const& operator [] (int index) const;

Та­ким обра­зом, ста­новит­ся оче­вид­но, что мы не можем сох­ранять стро­ку Юни­кода в виде char или wchar_t стро­ки. Нам нуж­но как минимум std::basic_string, пос­коль­ку на дан­ный момент кодиров­ки UTF-8 и UTF-16 кодиру­ют сим­волы в пре­делах int32_t, не говоря про UTF-32.

С дру­гой сто­роны, за пре­дела­ми клас­са text никому не нужен наш std::basic_string<int32_t>, назовем его unicode_string. Все биб­лиоте­ки любят работать с std::string и std::wstring или char const*иwchar_t const*`. Таким обра­зом, луч­ше все­го кеширо­вать как вхо­дящий std::string или std::wstring, так и резуль­тат переко­диров­ки тек­ста в кодиров­ку байт‑стро­ки. Мало того, час­то наш класс text понадо­бит­ся лишь как вре­мен­ное хра­нили­ще для путешес­тву­ющей стро­ки, нап­ример бай­товой в UTF-8 из базы дан­ных в JSON-стро­ку, то есть переко­диро­вание в unicode_string нам понадо­бит­ся лишь по тре­бова­нию обра­тить­ся к эле­мен­там — сим­волам тек­ста. Текст и его внут­реннее пред­став­ление — это тот класс, который дол­жен быть опти­мизи­рован по мак­симуму, так как пред­полага­ет интенсив­ное исполь­зование, а так­же не допус­кать переко­диро­вок без при­чины и до пер­вого тре­бова­ния. Поль­зователь API клас­са text дол­жен явно ука­зать, что хочет пре­обра­зовать текст в бай­товую стро­ку в опре­делен­ной кодиров­ке либо получить спе­цифич­ную для сис­темы «широкую» стро­ку:

std::string const& byte_string(std::string const& encoding) const;
std::wstring const& wide_string() const;

Как вид­но выше, мы воз­вра­щаем ссыл­ку на стро­ку, которую мы выс­читали и сох­ранили в поле клас­са. Разуме­ется, нам нуж­но будет почис­тить кеш с std::string и std::wstring при пер­вом же изме­нении зна­чения хотя бы одно­го сим­вола, здесь нам поможет operator → от некон­стантно­го this клас­са дан­ных text::data. Как это делать, смот­ри пре­дыду­щие два уро­ка ака­демии C++.

Нуж­но не забыть так­же и о получе­нии char const* и wchar_t const*, что нес­ложно дела­ется, учи­тывая то, что std::string и std::wstring кеширу­ются полями клас­са text.

char const* byte_c_str(char const* encoding) const;
wchar_t const* wide_c_str() const;

Ре­али­зация сво­дит­ся к вызову c_str() у резуль­татов byte_string и wide_string соот­ветс­твен­но.

Мож­но счи­тать кодиров­кой по умол­чанию для бай­товых строк UTF-8, это гораз­до луч­ше, чем пытать­ся работать с сис­темной кодиров­кой по умол­чанию, так код в зависи­мос­ти от сис­темы будет работать по‑раз­ному. Вве­дя ряд допол­нитель­ных перег­рузок без ука­зания кодиров­ки при работе с бай­товыми стро­ками, мы так­же получа­ем воз­можность пере­опре­делить опе­ратор прис­воения:

// в кодировке ”UTF-8”
text& operator = (std::string const& byte_string);
text& operator = (std::wstring const& wide_string);

Нуж­но так­же не забыть о перег­рузке опе­рато­ров + и +=, но в целом осталь­ные опе­рации мож­но уже сво­дить к аргу­мен­ту и резуль­тату типа text, уни­вер­саль­ному зна­чению, пре­дос­тавля­юще­му текст вне зависи­мос­ти от кодиров­ки.

Ра­зуме­ется, Ака­демия C++ не была бы ака­деми­ей, если бы я не пред­ложил тебе теперь реали­зовать класс тек­ста самос­тоятель­но. Поп­робуй соз­дать класс text на осно­ве матери­ала этой статьи. Реали­зация дол­жна удов­летво­рять двум прос­тым свой­ствам:

  • Клас­сом дол­жно быть удоб­нее поль­зовать­ся, чем стан­дар­тны­ми стро­ками, вдо­бавок класс пре­дос­тавля­ет сов­мести­мость либо вза­имное пре­обра­зова­ние с типами std::string, std::wstring, char const* и wchar_t const*.
  • Класс под­разуме­вает мак­сималь­ную опти­миза­цию, работа со стро­ками не дол­жна быть дороже, чем при работе со стан­дар­тны­ми std::string и std::wstring. То есть никаких неяв­ных переко­диро­вок, пока API явно не под­разуме­вает переко­диров­ку содер­жимого, ина­че клас­сом ник­то не будет поль­зовать­ся.

Здесь как раз име­ет смысл обра­ботать допол­нитель­но некон­стантный operator ->` для сбро­са кеша со стро­ками, одна­ко оставляю это на усмотре­ние раз­работ­чика. То есть тебя. Уда­чи!

Ра­зуме­ется, в реали­зации не обой­тись без клас­са copy_on_write из пре­дыду­щих ста­тей. Как обыч­но, на вся­кий слу­чай напоми­наю его упро­щен­ный вид:

template <class data_type>
class copy_on_write
{
public:
copy_on_write(data_type* data)
: m_data(data) {
}
data_type const* operator -> () const {
return m_data.get();
}
data_type* operator -> () {
if (!m_data.unique())
m_data.reset(new data_type(*m_data));
return m_data.get();
}
private:
std::shared_ptr<data_type> m_data;
};
 

Что мы получаем

Ре­али­зовав класс text, мы получим абс­трак­цию от мно­жес­тва кодиро­вок, все, что нам пот­ребу­ется, — одна перег­рузка от клас­са text. Нап­ример, так:

text to_json() const;
void from_json(text const& source);

Нам боль­ше не нуж­но мно­жес­тво перег­рузок от std::string и std::wstring, не нуж­но будет перехо­дить на под­дер­жку «широких» строк, дос­таточ­но заменить в API ссыл­ки на стро­ки на text, и получа­ем Юни­код авто­матом. Вдо­бавок мы получа­ем отличное кросс‑плат­формен­ное поведе­ние, вне зависи­мос­ти от того, какую биб­лиоте­ку мы выб­рали в качес­тве движ­ка переко­диров­ки, — ICU4C или libiconv, вви­ду того, что внут­реннее пред­став­ление у нас всег­да UTF-32 при рас­паков­ке сим­волов и мы ниг­де не завяза­ны на плат­формо­зави­симый wchar_t.

Ито­го: у нас есть сов­мести­мость либо вза­имо­кон­верта­ция со стан­дар­тны­ми типами, а зна­чит, и упро­щение под­дер­жки Юни­кода на сто­роне кода, написан­ного на С++. Ведь если мы пишем высоко­уров­невую логику на C++, мень­ше все­го нам хочет­ся получить проб­лемы при исполь­зовании wchar_t сим­волов и кучи одно­образно­го кода при обра­бот­ке и переко­диров­ке тек­ста.

При том что сама переко­диров­ка уже реали­зова­на в тех же ICU4C и libiconv, алго­ритм для внут­ренней работы клас­са text доволь­но прост. Дер­зай, и, может, уже зав­тра имен­но твоя биб­лиоте­ка работы с тек­стом будет исполь­зовать­ся пов­сюду в качес­тве высоко­уров­невой абс­трак­ции при обра­бот­ке любых тек­сто­вых дан­ных, от прос­того JSON с кли­ента до слож­ных тек­сто­вых струк­тур со сто­роны раз­личных баз дан­ных.

Оставить мнение