13

Scott Meyers による本「Effective STL」には、テキスト ファイル全体を std::string オブジェクトに読み込む良い例があります。

std::string sData; 

/*** Open the file for reading, binary mode ***/
std::ifstream ifFile (“MyFile.txt”, std::ios_base::binary); // Open for input, binary mode

/*** Read in all the data from the file into one string object ***/
sData.assign (std::istreambuf_iterator <char> (ifFile),
              std::istreambuf_iterator <char> ());

8 バイト文字として読み取ることに注意してください。これは非常にうまく機能します。最近では、Unicode テキスト (つまり、1 文字あたり 2 バイト) を含むファイルを読み取る必要があります。ただし、次のように、データを Unicode テキスト ファイルから std::wstring オブジェクトに読み込むように (単純に) 変更しようとすると、次のようになります。

std::wstring wsData; 

/*** Open the file for reading, binary mode ***/
std::wifstream ifFile (“MyFile.txt”, std::ios_base::binary); // Open for input, binary mode

/*** Read in all the data from the file into one string object ***/
wsData.assign (std::istreambuf_iterator <wchar_t> (ifFile),
               std::istreambuf_iterator <wchar_t> ());

返された文字列は、ワイド文字ですが、代替のヌルがまだ含まれています。たとえば、ファイルに Unicode 文字列「ABC」が含まれている場合、ファイルのバイト (Unicode 先頭バイト 0xFF、0xFE を無視) は次のようになります。 <'A'> <0> <'B'> <0> <' C'> <0>

上記の最初のコード フラグメントは、次の (char) 文字列の内容を正しく結果として返します。
sData [0] = 'A'<br> sData [1] = 0x00
sData [2] = 'B'<br> sData [3 ] = 0x00
sData [4] = 'C'<br> sData [5] = 0x00

ただし、2 番目のコード フラグメントが実行されると、(wchar_t) 文字列の内容が次のような望ましくない結果になります。
wsData [0] = L'A'<br> wsData [1] = 0x0000
wsData [2] = L'B '<br> wsData [3] = 0x0000
wsData [4] = L'C'<br> wsData [5] = 0x0000

あたかもファイルがまだバイトごとに読み取られ、単に個々の wchar_t 文字に変換されているかのようです。

wchar_t に特化した std::istreambuf_iterator は、ファイルが一度に 2 バイトずつ読み取られるべきだと思っていたのではないでしょうか? そうでない場合、その目的は何ですか?

私はテンプレートにたどり着きました(簡単な偉業ではありません;-)、イテレータは実際にファイルをバイトごとに読み取り、それを内部の変換ルーチンに渡しているようです。 2 バイトを受信した後でのみ)。

この一見些細なタスクについて、Web 上の多くのサイト (このサイトを含む) を検索しましたが、この動作の説明や、必要と思われる以上のコードを必要としない適切な代替案は見つかりませんでした (例: A Google Web を検索すると、同じ 2 番目のコード フラグメントが実行可能なコードとして生成されます)。

動作することがわかった唯一のものは次のとおりです。これは、wstring の内部バッファーに直接アクセスする必要があり、それを型強制する必要があるため、チートであると考えています。

std::wstring wsData; 

/*** Open the file for reading, binary mode ***/
std::wifstream ifFile (“MyFile.txt”, std::ios_base::binary); // Open for input, binary mode

wsData.resize (<Size of file in bytes> / sizeof (wchar_t));

ifFile.read ((char *) &wsData [0], <Size of file in bytes>);

ああ、避けられない「なぜファイルをバイナリ モードで開くのか、なぜテキスト モードで開くのか」という疑問を未然に防ぐために、ファイルがテキスト モード (デフォルト) で開かれたかのように意図的に開くということは、CR/LF ("\ r\n" または 0x0D0A) シーケンスは LF ("\n" または 0x0A) シーケンスに変換されますが、ファイルの純粋なバイト読み取りではそれらが保持されます。いずれにせよ、それらの頑固者にとって、それを変更しても、当然のことながら、何の効果もありませんでした.

ここで 2 つの質問があります。2 番目のケースが期待どおりに機能しないのはなぜですか (つまり、これらの反復子で何が起こっているのか)、そして Unicode 文字のファイルを wstring にロードする際のお気に入りの「コーシャ STL 方法」は何ですか? ?

ここで何が欠けていますか。それはばかげたものでなければなりません。

クリス

4

1 に答える 1

12

4 か月半経っても最初の質問に対する回答が得られなかったことに、SO はがっかりしているに違いありません。これは良い質問であり、良い質問のほとんどは (よくも悪くも) 数分以内に回答されます。あなたのことを無視する理由として考えられるのは、次の 2 つです。

  • 「C++」というタグを付けなかったので、助けられたかもしれない多くの C++ プログラマーはそれに気付かなかったでしょう。(「C++」とタグ付けしました。)

  • あなたの質問は、ユニコード ストリーム処理に関するものです。これは、クールなコーディングのアイデアではありません。

std::wfstreamあなたの調査を妨害した誤解は次のようです: あなたは、ワイド文字ストリームとワイド文字列std::wstringがそれぞれ「ユニコード ストリーム」と「ユニコード文字列」と同じであると信じているようです。具体的にはそれぞれ UTF-16 ストリームおよび UTF-16 文字列と同じであること。これらのことはどちらも真実ではありません。

( std::wifstream)は、外部シーケンスの指定またはデフォルトのエンコーディングに従ってstd::basic_ifstream<wchar_t>外部バイト シーケンスを の内部シーケンスに変換する入力ストリームです。wchar_t

同様に、std::wofstream( ) は、外部シーケンスの指定またはデフォルトのエンコーディングに従って、 の内部シーケンスを外部バイト シーケンスstd::basic_ofstream<wchar_t>に変換する出力ストリームです。wchar_t

そして、std::wstring( ) は、その結果のエンコーディング (もしあれば) を知らずに、std::basic_string<wchar_t>単に のシーケンスを格納する文字列型です。wchar_t

Unicodeはバイト シーケンス エンコーディングのファミリです - UTF-8/-16/-32、およびいくつかのよりあいまいなエンコーディングは、UTF- Nがシンボルごとに 1 つ以上の N ビット単位のシーケンスを使用してアルファベットをエンコードする という原則によって関連付けられています。UTF-16 は、明らかに、に読み込もうとしているエンコーディングです。あなたは言う:std::wstring

wchar_t に特化した std::istreambuf_iterator を使用すると、ファイルが一度に 2 バイトずつ読み込まれるはずだと思っていたのではないでしょうか。そうでない場合、その目的は何ですか?

しかし、それwchar_tが必ずしも 2 バイト幅であるとは限らず (Microsoft の C ライブラリには 32 ビットと 64 ビットの両方がありますが、GCC では 4 バイト幅です)、また UTF-16 コードポイント (文字) が必要であることを知っていれば、 2 バイトに収まらない場合 (4 バイトが必要な場合もあります)、wchar_tUTF-16 ストリームをデコードするには、抽出単位を指定するだけでは不十分であることがわかります。

入力ストリームを作成して開く場合:

std::wifstream ifFile ("MyFile.txt", std::ios_base::binary);

「MyFile.txt」から文字 (一部のアルファベット) をタイプの値に抽出する準備ができて おり、ストリーム上で動作するwchar_tによって指定されたエンコーディングに従って、ファイル内のバイト シーケンスからそれらの文字を抽出します。std::locale抽出を行います。

コードstd::localeでストリームに が指定されていないため、ライブラリのデフォルトが有効になります。そのデフォルトはグローバル C++ ロケールであり、デフォルトでは "C" ロケールです。また、"C" ロケールは、I/O バイト シーケンスの "identity encoding"、つまり 1 バイト = 1 文字 (テキスト モード I/O の改行例外を脇に置く) を想定しています。

したがって、 を使用std::istreambuf_iterator<wchar_t>して文字を抽出すると、ファイル内の各バイトが に変換され、wchar_tそれが に追加されstd::wstring wsDataます。あなたが言うように、ファイル内のバイトは次のとおりです。

0xFF、0xFE、「A」、0x00、「B」、0x00、「C」、0x00

「Unicodeリードバイト」として割引する最初の2つは、実際にはUTF-16バイトオーダーマーク(BOM)ですが、デフォルトのエンコーディングでは、それらはそのままです。

wsDataしたがって、あなたが観察したように、に割り当てられたワイド文字は次のとおりです。

0x00FF、0x00FE、L'A'、0x0000、L'B'、0x0000、L'C'、0x0000

あたかもファイルがまだバイトごとに読み取られ、単に個々の wchar_t 文字に変換されているかのようです。

まさにそれが起こっているからです。

これを防ぐには、ストリームから文字を抽出する前に、UTF-16 文字シーケンスをデコードすることになっていることを伝える必要があります。それを行う方法は、概念的にかなり曲がりくねっています。UTF-16 を にデコードする正しいメソッドをストリームに提供する、インスタンス化された (またはインスタンス化 から派生した)を所有する をimbue 使用してストリームする必要があります。std::localestd::locale::facetstd::codecvt<InternT, ExternT, StateT>wchar_t

しかし、これの要点は、適切な UTF-16 エンコーダー/デコーダーをストリームにプラグインする必要があり、実際には十分に単純である (またはそうあるべきである) ということです。あなたのコンパイラは最近の MS VC++ だと思います。それが正しい場合は、次の方法でコードを修正できます。

  • ヘッダーに#include <locale>とを追加する#include <codecvt>
  • 次の行を追加します。

    ifFile.imbue(std::locale(ifFile.getloc(),new std::codecvt_utf16<wchar_t,0x10ffff,std::little_endian>));

直後の:

std::wifstream ifFile ("MyFile.txt", std::ios_base::binary);

この新しい行の効果はifFile、それがすでに持っていたものと同じ新しいロケールを「吹き込む」ことですが、ifFile.getloc()エンコーダー/デコーダーのファセットが変更されていstd::codecvt_utf16<wchar_t,0x10ffff,std::little_endian>ます。このファセットは、最大値 の UTF-16 文字をリトル エンディアン 値 ( UTF-16 コード ポイントの最大値) にcodecvtデコードするファセットです。0x10ffffwchar_t0x10ffff

wsDataこのように修正されたコードをデバッグすると、長さがわずか 4 ワイド文字であり、それらの文字が次のようになっていることがわかります。

0xFEFF, L'A', L'B', L'C'

最初のものは UTF-16 リトルエンディアン BOM です。

FE順序 がファセットの適用前とFFは逆になっていることに注意してください。これはcodecvt、リトルエンディアンのデコードが要求どおりに行われたことを示しています。そして、そうである必要がありました。を削除して新しい行を編集し、std::little_endian再度デバッグすると、 の最初の要素がwsData変わり0xFFFE 、他の 3 つのワイド文字が IICoreの絵文字セットの絵文字になることがわかります (デバッガーがそれらを表示できる場合)。(これで、同僚が彼らのコードが英語の Unicode を「中国語」に変えていることに驚いて不平を言うときはいつでも、あなたはありそうな説明を知っているでしょう.)

先頭の BOM なしで入力したい場合wsDataは、新しい行を再度修正してstd::little_endianstd::codecvt_mode(std::little_endian|std::consume_header)

wchar_t 最後に、新しいコードのバグに気付いたかもしれません。つまり、読み取り可能な 0x100000 と 0x10ffff の間の UTF-16 コードポイントを表すには、2 バイトでは幅が不十分であるということです。

読み取る必要があるすべてのコードポイントが[0,0xffff] にまたがるUTF-16 Basic Multilingual Planeにある限り、これでうまくいきます。また、すべての入力が永遠にその制約に従うことを知っているかもしれません。それ以外の場合、16 ビットwchar_tは目的に適合しません。交換:

  • wchar_tchar32_t
  • std::wstringstd::basic_string<char32_t>
  • std::wifstreamstd::basic_ifstream<char32_t>

コードは、任意の UTF-16 でエンコードされたファイルを文字列に読み取るのに完全に適しています。

(GNU C++ ライブラリを使用している読者は、v4.7.2 の時点で<codecvt>標準ヘッダーがまだ提供されていないことに気付くでしょう。ヘッダー<bits/codecvt.h>は存在し、おそらくいずれ卒業し て<codecvt>.これはそれぞれ、ID 変換と ASCII/UTF-8 と の間の変換です。OP の問題を解決するには、この回答 に従って、自分でサブクラス化する必要があります) 。class codecvt<char, char, mbstate_t>class codecvt<wchar_t, char, mbstate_t>wchar_tstd::codecvt<wchar_t,char,std::char_traits<wchar_t>::state_type>

于 2013-05-20T17:10:44.957 に答える