Unicode được hỗ trợ tốt như thế nào trong C ++ 11?


183

Tôi đã đọc và nghe rằng C ++ 11 hỗ trợ Unicode. Một vài câu hỏi về điều đó:

  • Thư viện chuẩn C ++ hỗ trợ Unicode tốt như thế nào?
  • std::stringlàm những gì nó nên?
  • Làm thế nào để tôi sử dụng nó?
  • Vấn đề tiềm ẩn ở đâu?

19
"Std :: string có làm những gì cần không?" Bạn nghĩ nó nên làm gì?
R. Martinho Fernandes

2
Tôi sử dụng utfcpp.sourceforge.net cho nhu cầu utf8 của mình. Đây là một tệp tiêu đề đơn giản cung cấp các trình vòng lặp cho các chuỗi unicode.
fscan

2
std :: chuỗi nên lưu trữ byte, tức là chuỗi đơn vị mã của mã hóa UTF-8. Vâng, nó chỉ làm điều đó, kể từ đầu. utf8everywhere.org
Pavel Radzivilovsky

3
Các vấn đề tiềm năng lớn nhất với hỗ trợ Unicode nằm trong Unicode và việc sử dụng nó trong công nghệ thông tin. Unicode không phù hợp (và không được thiết kế) cho những gì nó được sử dụng cho. Unicode được thiết kế để tái tạo mọi glyph có thể được viết ở đâu đó bởi một người nào đó, đôi khi với mọi sắc thái không thể và mang tính mô phạm có thể, bao gồm 3 hoặc 4 ý nghĩa khác nhau và 3 hoặc 4 cách khác nhau để tạo ra cùng một glyph. Nó không có nghĩa là hữu ích cho việc sử dụng cho ngôn ngữ hàng ngày và nó không có nghĩa là có thể áp dụng hoặc được xử lý dễ dàng hoặc rõ ràng.
Damon

11
Có nó được thiết kế để được sử dụng cho ngôn ngữ hàng ngày. Của tôi ít nhất. Và bạn có lẽ cũng vậy. Nó chỉ ra rằng xử lý văn bản của con người một cách chung chung là một nhiệm vụ rất khó khăn. Thậm chí không thể định nghĩa rõ ràng một nhân vật là gì. Tái tạo glyph nói chung thậm chí không thực sự là một phần của điều lệ Unicode.
Jean-Denis Muys

Câu trả lời:


267

Thư viện chuẩn C ++ hỗ trợ unicode tốt như thế nào?

Kinh khủng

Quét nhanh qua các phương tiện thư viện có thể cung cấp hỗ trợ Unicode cho tôi danh sách này:

  • Thư viện chuỗi
  • Thư viện địa phương
  • Thư viện đầu vào / đầu ra
  • Thư viện biểu thức chính quy

Tôi nghĩ tất cả nhưng cái đầu tiên cung cấp hỗ trợ khủng khiếp. Tôi sẽ lấy lại chi tiết hơn sau khi đi nhanh qua các câu hỏi khác của bạn.

std::stringlàm những gì nó nên?

Đúng. Theo tiêu chuẩn C ++, đây là những gì std::stringvà anh chị em của nó nên làm:

Mẫu lớp basic_stringmô tả các đối tượng có thể lưu trữ một chuỗi bao gồm một số lượng khác nhau các đối tượng giống như char tùy ý với phần tử đầu tiên của chuỗi ở vị trí 0.

Vâng, std::stringđiều đó chỉ tốt thôi. Điều đó có cung cấp bất kỳ chức năng cụ thể nào cho Unicode không? Không.

Có nên không? Chắc là không. std::stringlà tốt như một chuỗi các charđối tượng. Điều đó hữu ích; điều khó chịu duy nhất là nó là một chế độ xem văn bản ở mức độ rất thấp và C ++ tiêu chuẩn không cung cấp mức độ cao hơn.

Làm thế nào để tôi sử dụng nó?

Sử dụng nó như một chuỗi các charđối tượng; giả vờ nó là một cái gì đó khác chắc chắn sẽ kết thúc trong đau đớn.

Vấn đề tiềm ẩn ở đâu?

Khắp nơi? Hãy xem nào...

Thư viện chuỗi

Thư viện chuỗi cung cấp cho chúng ta basic_string, đây chỉ là một chuỗi những gì mà tiêu chuẩn gọi là "các đối tượng giống như char". Tôi gọi họ là đơn vị mã. Nếu bạn muốn có một cái nhìn văn bản cấp cao, đây không phải là thứ bạn đang tìm kiếm. Đây là một khung nhìn của văn bản phù hợp cho việc tuần tự hóa / giải tuần tự hóa / lưu trữ.

Nó cũng cung cấp một số công cụ từ thư viện C có thể được sử dụng để thu hẹp khoảng cách giữa thế giới hẹp và thế giới Unicode: c16rtomb/ mbrtoc16c32rtomb/mbrtoc32 .

Thư viện địa phương

Thư viện bản địa hóa vẫn tin rằng một trong những "vật thể giống như char" đó bằng một "ký tự". Điều này tất nhiên là ngớ ngẩn, và làm cho không thể có nhiều thứ hoạt động chính xác ngoài một số tập hợp nhỏ của Unicode như ASCII.

Ví dụ, hãy xem xét những gì tiêu chuẩn gọi là "giao diện tiện lợi" trong <locale>tiêu đề:

template <class charT> bool isspace (charT c, const locale& loc);
template <class charT> bool isprint (charT c, const locale& loc);
template <class charT> bool iscntrl (charT c, const locale& loc);
// ...
template <class charT> charT toupper(charT c, const locale& loc);
template <class charT> charT tolower(charT c, const locale& loc);
// ...

Làm thế nào để bạn mong đợi bất kỳ chức năng nào trong số này để phân loại chính xác, giả sử, U + 1F34C, như trong u8"🍌"hoặc u8"\U0001F34C"? Không có cách nào nó sẽ hoạt động, bởi vì các hàm đó chỉ lấy một đơn vị mã làm đầu vào.

Điều này có thể hoạt động với một miền thích hợp nếu bạn char32_tchỉ sử dụng :U'\U0001F34C' là một đơn vị mã duy nhất trong UTF-32.

Tuy nhiên, điều đó vẫn có nghĩa là bạn chỉ nhận được các phép biến đổi vỏ đơn giản với touppertolower, ví dụ, không đủ tốt cho một số ngôn ngữ Đức: "ß" chữ hoa thành "SS" nhưng toupperchỉ có thể trả về một đơn vị mã ký tự .

Lên tiếp theo, wstring_convert/ wbuffer_convertvà các khía cạnh chuyển đổi mã tiêu chuẩn.

wstring_convertđược sử dụng để chuyển đổi giữa các chuỗi trong một mã hóa đã cho thành các chuỗi trong một mã hóa đã cho khác. Có hai loại chuỗi liên quan đến phép chuyển đổi này, mà tiêu chuẩn gọi là chuỗi byte và chuỗi rộng. Vì các thuật ngữ này thực sự sai lệch, tôi thích sử dụng "tuần tự hóa" và "giải tuần tự hóa", thay vào đó.

Các mã hóa để chuyển đổi giữa được quyết định bởi một codecvt (một khía cạnh chuyển đổi mã) được truyền dưới dạng đối số kiểu mẫu tới wstring_convert.

wbuffer_convertthực hiện một chức năng tương tự nhưng như một bộ đệm luồng khử lưu lượng rộng bao bọc một bộ đệm dòng tuần tự byte . Bất kỳ I / O nào cũng được thực hiện thông qua bộ đệm dòng tuần tự byte cơ bản với các chuyển đổi đến và từ các mã hóa được đưa ra bởi đối số codecvt. Viết tuần tự vào bộ đệm đó, sau đó viết từ nó, và đọc đọc vào bộ đệm và sau đó giải tuần tự từ nó.

Tiêu chuẩn này cung cấp một số lớp mẫu codecvt để sử dụng với các cơ sở này: codecvt_utf8, codecvt_utf16, codecvt_utf8_utf16, và một số codecvtchuyên ngành. Các khía cạnh tiêu chuẩn này cung cấp tất cả các chuyển đổi sau đây. (Lưu ý: trong danh sách sau đây, mã hóa ở bên trái luôn là chuỗi / streambuf được tuần tự hóa và mã hóa ở bên phải luôn là chuỗi / streambuf được giải tuần tự; tiêu chuẩn cho phép chuyển đổi theo cả hai hướng).

  • UTF-8 UCS-2 với codecvt_utf8<char16_t>, và codecvt_utf8<wchar_t>ở đâu sizeof(wchar_t) == 2;
  • UTF-8 ↔ UTF-32 với codecvt_utf8<char32_t>, codecvt<char32_t, char, mbstate_t>codecvt_utf8<wchar_t>nơi sizeof(wchar_t) == 4;
  • UTF-16 UCS-2 với codecvt_utf16<char16_t>, và codecvt_utf16<wchar_t>ở đâu sizeof(wchar_t) == 2;
  • UTF-16 UTF-32 với codecvt_utf16<char32_t>, và codecvt_utf16<wchar_t>ở đâu sizeof(wchar_t) == 4;
  • UTF-8 ↔ UTF-16 với codecvt_utf8_utf16<char16_t>, codecvt<char16_t, char, mbstate_t>codecvt_utf8_utf16<wchar_t>nơi sizeof(wchar_t) == 2;
  • hẹp rộng với codecvt<wchar_t, char_t, mbstate_t>
  • không-op với codecvt<char, char, mbstate_t>.

Một vài trong số này là hữu ích, nhưng có rất nhiều thứ khó xử ở đây.

Trước hết hãy thay thế thánh cao cấp! kế hoạch đặt tên đó là lộn xộn.

Sau đó, có rất nhiều hỗ trợ UCS-2. UCS-2 là một mã hóa từ Unicode 1.0 được thay thế vào năm 1996 vì nó chỉ hỗ trợ mặt phẳng đa ngôn ngữ cơ bản. Tại sao ủy ban nghĩ rằng mong muốn tập trung vào một mã hóa đã được thay thế hơn 20 năm trước, tôi không biết. Nó không giống như hỗ trợ cho nhiều mã hóa là xấu hoặc bất cứ điều gì, nhưng UCS-2 xuất hiện quá thường xuyên ở đây.

Tôi muốn nói rằng điều đó char16_trõ ràng có nghĩa là để lưu trữ các đơn vị mã UTF-16. Tuy nhiên, đây là một phần của tiêu chuẩn nghĩ khác. codecvt_utf8<char16_t>không có gì để làm với UTF-16. Ví dụ, wstring_convert<codecvt_utf8<char16_t>>().to_bytes(u"\U0001F34C")sẽ biên dịch tốt, nhưng sẽ thất bại vô điều kiện: đầu vào sẽ được coi là chuỗi UCS-2 u"\xD83C\xDF4C", không thể chuyển đổi thành UTF-8 vì UTF-8 không thể mã hóa bất kỳ giá trị nào trong phạm vi 0xD800-0xDFFF.

Vẫn ở mặt trước UCS-2, không có cách nào để đọc từ luồng byte UTF-16 thành chuỗi UTF-16 với các khía cạnh này. Nếu bạn có một chuỗi các UTF-16 byte, bạn không thể giải tuần tự hóa nó thành một chuỗi char16_t. Điều này là đáng ngạc nhiên, bởi vì nó ít nhiều là một chuyển đổi nhận dạng. Tuy nhiên, thậm chí còn đáng ngạc nhiên hơn là thực tế là có hỗ trợ khử lưu lượng từ luồng UTF-16 thành chuỗi UCS-2 với codecvt_utf16<char16_t>, đây thực sự là một chuyển đổi mất mát.

Mặc dù vậy, hỗ trợ UTF-16-byte khá tốt: nó hỗ trợ phát hiện endianess từ BOM hoặc chọn nó một cách rõ ràng trong mã. Nó cũng hỗ trợ sản xuất đầu ra có và không có BOM.

Có một số khả năng chuyển đổi thú vị hơn vắng mặt. Không có cách nào để giải tuần tự hóa từ luồng hoặc chuỗi byte UTF-16 thành chuỗi UTF-8, vì UTF-8 không bao giờ được hỗ trợ dưới dạng khử lưu huỳnh.

Và ở đây, thế giới hẹp / rộng hoàn toàn tách biệt với thế giới UTF / UCS. Không có chuyển đổi giữa các bảng mã hẹp / rộng kiểu cũ và bất kỳ bảng mã Unicode nào.

Thư viện đầu vào / đầu ra

Thư viện I / O có thể được sử dụng để đọc và viết văn bản trong bảng mã Unicode bằng cách sử dụng wstring_convertwbuffer_convertcác phương tiện được mô tả ở trên. Tôi không nghĩ rằng có nhiều thứ khác cần được hỗ trợ bởi phần này của thư viện chuẩn.

Thư viện biểu thức chính quy

Tôi đã giải thích các vấn đề với biểu thức chính quy C ++ và Unicode trên Stack Overflow trước đây. Tôi sẽ không lặp lại tất cả những điểm đó ở đây, mà chỉ nói rằng các biểu thức C ++ không có hỗ trợ Unicode cấp 1, đây là mức tối thiểu để chúng có thể sử dụng được mà không cần sử dụng UTF-32 ở mọi nơi.

Đó là nó?

Vâng, đó là nó. Đó là chức năng hiện có. Có rất nhiều chức năng Unicode mà không nơi nào có thể nhìn thấy như các thuật toán phân đoạn văn bản hoặc bình thường hóa.

U + 1F4A9 . Có cách nào để có được một số hỗ trợ Unicode tốt hơn trong C ++ không?

Các nghi phạm thông thường: ICUBoost.Locale .


Một chuỗi byte là, không có gì đáng ngạc nhiên, một chuỗi các byte, tức là charcác đối tượng. Tuy nhiên, không giống như một chuỗi ký tự rộng , luôn luôn là một mảng các wchar_tđối tượng, một "chuỗi rộng" trong ngữ cảnh này không nhất thiết là một chuỗi các wchar_tđối tượng. Trong thực tế, tiêu chuẩn không bao giờ định nghĩa rõ ràng "chuỗi rộng" nghĩa là gì, vì vậy chúng tôi còn lại để đoán ý nghĩa từ việc sử dụng. Vì thuật ngữ tiêu chuẩn là cẩu thả và khó hiểu, tôi sử dụng của riêng tôi, nhân danh sự rõ ràng.

Các mã hóa như UTF-16 có thể được lưu trữ dưới dạng các chuỗi char16_t, sau đó không có tuổi thọ; hoặc chúng có thể được lưu trữ dưới dạng chuỗi byte, có tuổi thọ (mỗi cặp byte liên tiếp có thể biểu thị một char16_tgiá trị khác nhau tùy thuộc vào tuổi thọ). Tiêu chuẩn hỗ trợ cả hai hình thức này. Một chuỗi char16_thữu ích hơn cho thao tác nội bộ trong chương trình. Một chuỗi các byte là cách để trao đổi các chuỗi như vậy với thế giới bên ngoài. Các thuật ngữ tôi sẽ sử dụng thay vì "byte" và "wide" là "tuần tự hóa" và "giải tuần tự hóa".

Nếu bạn định nói "nhưng Windows!" giữ 🐎🐎 của bạn . Tất cả các phiên bản Windows kể từ Windows 2000 đều sử dụng UTF-16.

☦ Có, tôi biết về các Eszett (ẞ), nhưng ngay cả khi bạn thay đổi tất cả các địa phương của Đức qua đêm để có chữ hoa thành, vẫn còn nhiều trường hợp khác sẽ thất bại. Hãy thử tải lên U + FB00 ʟᴀᴛɪɴ sᴍᴀʟʟ ʟɪɢᴀᴛᴜʀᴇ. Không có ʟᴀᴛɪɴ ᴄᴀᴘɪᴛᴀʟ ғғ; nó chỉ viết hoa lên hai chữ F. Hoặc U + 01F0 sᴍᴀʟʟ ʟᴇᴛᴛᴇʀ ᴊ ᴡɪᴛʜ ᴄᴀʀᴏɴ; không có vốn đầu tư; nó chỉ viết hoa cho chữ J viết hoa và kết hợp caron.


26
Càng đọc về nó, tôi càng có cảm giác không hiểu gì về tất cả những điều này. Tôi đã đọc hầu hết những thứ này một vài tháng trước và vẫn cảm thấy như mình đang khám phá lại tất cả mọi thứ một lần nữa ... Để đơn giản cho bộ não tội nghiệp của tôi bây giờ bị tổn thương một chút, tất cả những lời khuyên trên utf8every vẫn còn hiệu lực, đúng? Nếu tôi "chỉ" muốn người dùng của mình có thể mở và ghi tệp bất kể cài đặt hệ thống của họ, tôi có thể hỏi họ tên tệp, lưu trữ trong chuỗi std :: và mọi thứ sẽ hoạt động bình thường, ngay cả trên Windows? Xin lỗi khi hỏi điều đó (một lần nữa) ...
Uflex

5
@Uflex Tất cả những gì bạn thực sự có thể làm với std :: string là coi nó như một blob nhị phân. Trong một triển khai Unicode thích hợp, cả nội bộ (vì nó ẩn sâu trong các chi tiết triển khai) cũng như các vấn đề mã hóa bên ngoài (tốt, sắp xếp, bạn vẫn cần phải có sẵn bộ mã hóa / giải mã).
Cat Plus Plus

3
@Uflex có lẽ. Tôi không biết nếu làm theo lời khuyên mà bạn không hiểu là một ý tưởng tốt.
R. Martinho Fernandes

1
Có một đề xuất hỗ trợ Unicode trong C ++ 2014/17. Tuy nhiên, đó là 1, có thể là 4 năm và ít sử dụng bây giờ. open-std.org/jtc1/sc22/wg21/docs/ con / 2013 / n3572.html
graham.reeds

20
@ graham.reeds haha, cảm ơn, nhưng tôi đã nhận thức được điều đó. Kiểm tra phần "Lời cảm ơn";)
R. Martinho Fernandes

40

Unicode không được Thư viện tiêu chuẩn hỗ trợ (đối với bất kỳ ý nghĩa hợp lý nào được hỗ trợ).

std::stringkhông tốt hơn std::vector<char>: nó hoàn toàn không biết về Unicode (hoặc bất kỳ biểu diễn / mã hóa nào khác) và chỉ đơn giản coi nội dung của nó là một đốm byte.

Nếu bạn chỉ cần lưu trữ và catenate blobs , nó hoạt động khá tốt; nhưng ngay khi bạn muốn chức năng Unicode (số điểm mã , số biểu đồ, v.v.), bạn đã hết may mắn.

Thư viện toàn diện duy nhất tôi biết cho việc này là ICU . Giao diện C ++ được lấy từ Java, vì vậy nó không phải là thành ngữ.


2
Làm thế nào về Boost.Locale ?
Uflex

11
@Uflex: từ trang bạn đã liên kết Để đạt được mục tiêu này Boost.Locale sử dụng thư viện Unicode và Bản địa hóa hiện đại: ICU - Thành phần quốc tế cho Unicode.
Matthieu M.

1
Boost.Locale hỗ trợ backends phi ICU khác, xem tại đây: boost.org/doc/libs/1_53_0/libs/locale/doc/html/...
Superfly Jon

@SuperflyJon: Đúng, nhưng theo cùng một trang đó, sự hỗ trợ cho Unicode của các phụ trợ không phải ICU là "bị giới hạn nghiêm trọng".
Matthieu M.

24

Bạn có thể lưu trữ UTF-8 một cách an toàn trong một std::string(hoặc trong một char[]hoặc char*, đối với vấn đề đó), do thực tế là Unicode NUL (U + 0000) là một byte rỗng trong UTF-8 và đây là cách duy nhất là null byte có thể xảy ra trong UTF-8. Do đó, các chuỗi UTF-8 của bạn sẽ được kết thúc đúng theo tất cả các hàm chuỗi C và C ++ và bạn có thể kết nối chúng với các iostream C ++ (bao gồm std::coutstd::cerr, miễn là ngôn ngữ của bạn là UTF-8).

Những gì bạn không thể làm với std::stringUTF-8 là lấy độ dài của các điểm mã. std::string::size()sẽ cho bạn biết độ dài chuỗi tính bằng byte , chỉ bằng số điểm mã khi bạn nằm trong tập hợp con ASCII của UTF-8.

Nếu bạn cần hoạt động trên các chuỗi UTF-8 ở cấp điểm mã (nghĩa là không chỉ lưu trữ và in chúng) hoặc nếu bạn đang xử lý UTF-16, có khả năng có nhiều byte rỗng bên trong, bạn cần xem xét các loại chuỗi ký tự rộng.


3
std::stringcó thể được ném vào iostream với các null được nhúng tốt.
R. Martinho Fernandes

3
Đó là hoàn toàn dự định. Nó hoàn toàn không phá vỡ c_str()size()vẫn hoạt động. Chỉ các API bị hỏng (tức là các API không thể xử lý các null được nhúng như hầu hết thế giới C) mới bị phá vỡ.
R. Martinho Fernandes

1
Các null được nhúng bị phá vỡ c_str()c_str()được cho là trả về dữ liệu dưới dạng chuỗi C kết thúc null --- điều này là không thể, do thực tế là các chuỗi C không thể nhúng null.
uckelman

4
Không còn nữa. c_str()bây giờ chỉ đơn giản là trả về giống như data(), tức là tất cả của nó. API có kích thước có thể tiêu thụ nó. API không, không thể.
R. Martinho Fernandes

6
Với sự khác biệt nhỏ c_str()đảm bảo kết quả được theo sau bởi một đối tượng giống như NUL và tôi không nghĩ data()vậy. Không, có vẻ như data()bây giờ cũng làm điều đó. (Tất nhiên, điều này là không cần thiết đối với các API tiêu thụ kích thước thay vì suy ra từ tìm kiếm của kẻ hủy diệt)
Ben Voigt

8

C ++ 11 có một vài loại chuỗi ký tự mới cho Unicode.

Thật không may, sự hỗ trợ trong thư viện tiêu chuẩn cho các bảng mã không đồng nhất (như UTF-8) vẫn còn tệ. Ví dụ, không có cách nào hay để lấy độ dài (tính theo điểm mã) của chuỗi UTF-8.


Vì vậy, chúng ta vẫn cần sử dụng std :: wopes cho tên tệp nếu chúng ta muốn hỗ trợ các ngôn ngữ không phải là tiếng Latin? Bởi vì chuỗi ký tự mới không thực sự giúp ích ở đây vì chuỗi thường đến từ người dùng ...
Uflex

7
@Uflex std::stringcó thể giữ chuỗi UTF-8 mà không gặp vấn đề gì, nhưng ví dụ lengthphương thức trả về số byte trong chuỗi chứ không phải số điểm mã.
Một số lập trình viên anh chàng

8
Thành thật mà nói, việc lấy độ dài của các điểm mã của chuỗi không có nhiều cách sử dụng. Chẳng hạn, độ dài tính bằng byte để phân bổ bộ đệm chính xác.
R. Martinho Fernandes

2
Số lượng điểm mã trong chuỗi UTF-8 không phải là một con số rất thú vị: Người ta có thể viết ñlà 'LATIN SMALL LETTER N VỚI TILDE' (U + 00F1) (là một điểm mã) hoặc 'LATIN SMALL LALLTER N' ( U + 006E) theo sau là 'COMBINING TILDE' (U + 0303) là hai điểm mã.
Martin Bonner hỗ trợ Monica

Tất cả những nhận xét về "bạn không cần điều này và bạn không cần số" như "số điểm mã không quan trọng", v.v ... nghe có vẻ hơi tanh với tôi. Khi bạn viết một trình phân tích cú pháp được cho là phân tích mã nguồn utf8 của các loại, tùy thuộc vào đặc tả của trình phân tích cú pháp cho dù nó có xem xét LATIN SMALL LETTER N' == hay không (U+006E) followed by 'COMBINING TILDE' (U+0303).
BitTickler

4

Tuy nhiên, có một thư viện khá hữu ích được gọi là tiny-utf8 , về cơ bản là một thay thế thả vào cho std::string/ std::wstring. Nó nhằm mục đích lấp đầy khoảng trống của lớp container utf8 vẫn còn thiếu.

Đây có thể là cách 'giao dịch' thoải mái nhất với các chuỗi utf8 (nghĩa là không có chuẩn hóa unicode và các công cụ tương tự). Bạn thoải mái hoạt động trên các điểm mã , trong khi chuỗi của bạn được mã hóa theo chars được mã hóa theo chiều dài .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.