前置き
話を始める前にBOMとは
バイトオーダーマーク (byte order mark)のことです。間違ってもBill of materialsとか東方シューティングのスペカでは無いです。
Unicodeを知らないプログラマは流石に存在しないと思うので割愛して、BOMとは一般にはファイルの先頭につけるもので
-
UTF-8
/UTF-16
/UTF-32
-
little endianness
/big endianness
(UTF-8は除く)
を判別するためにあります。やべぇ、わからん、という人は
UTF-32 is NOT a fixed-legnth character encoding | 本の虫
を見ましょう。
世の中のテキストファイルは大体UTF-8 with BOMだ(という幻想)
という幻想を信じるならバイナリエディタで覗くと
EF BB BF
でファイルが始まることになります。
ところでみんな、C++のソースコードはもちろんUTF-8 with BOMだよね?間違ってもShift-JISじゃないよね?(きっとそれをつかう職場もあるだろうな)
ところでC++でファイルってどう開くよ?
まさかfopenを使うC++erはいないと信じて(きっとそれをつかう職場もあるだろうな)
#include <fstream>
int main()
{
std::ifstream file("arikitari_na_text.txt");
if(!file) return -1;
//なんか
}
これまで私がやってたbom skip(UTF-8 only)
#include <fstream>
#include <algorithm>
#include <codecvt>
void skip_utf8_bom(std::ifstream& fs) {
int dst[3];
for (auto& i : dst) i = fs.get();
constexpr int utf8[] = { 0xEF, 0xBB, 0xBF };
if (!std::equal(std::begin(dst), std::end(dst), utf8)) fs.seekg(0);
}
int main()
{
std::ifstream file;
file.open("arikitari_na_text.txt");
file.imbue(std::locale());
skip_utf8_bom(file);
if(!file) return -1;
//なんか
}
この方法はwfstreamに適応できないので、それを使うには一回bom skipしてcode_cvt噛ませつつwstringstreamにいれて、とかいうよくわかんないことをやってた。
今回紹介する方法
ネタ元は忘れた。どっかのStack Overflowに書いてあった気がする。
#include <fstream>
#include <codecvt>
int main()
std::wifstream file;
file.open("arikitari_na_text.txt");
static_assert(sizeof(wchar_t) == 2, "error.");//Linuxではつかうcvt違うから直してくれ
file.imbue(std::locale(std::locale(""), new std::codecvt_utf8_utf16<wchar_t, 0x10ffff, std::consume_header>()));
if(!file) return -1;
//なんか
}
std::fstream::imbue()に渡すlocaleに渡すcodecvtのtemplate第三引数にstd::consume_header
を渡すのが肝です。
結論
C++11が使える環境ではbom skipも幸せだということです。いい加減C++03は卒業しましょう。(きっとできない職場もあるだろうな)
余談
試してないけど、BOMを逆に書き出すにはstd::generate_header
っていうのがあるっぽいよ。
にしてもそろそろ標準で自動文字コード判別してくれないかな・・・。BOMあるときだけでいいからさ
経緯
@yumetodo picojsonの前にcsvデータの方をUTF-8にしようとしたら、ifsからgetlineでstringに読みこんだデータが壊れているせいでSplitしようがないのですけれど、この場合はどうしましょう? pic.twitter.com/2wVqtGdswL
— YSR@孤高の人間観察好き㌠ (@YSRKEN) 2016年2月27日
@YSRKEN そのまま問題なく読み込めていると思います。(BOMskipしてるなら)
— yumetodo-C++erだけど化学科 (@yumetodo) 2016年2月27日
VSのデバッガはchar幅の変数はshft-JISとして表示するのでバグって見えるだけじゃないかと。
そのまま読み込んでwstring_convertすればいいと思います
@yumetodo つまり読み込んだら速攻でwstring_convertしてから、wstring前提で作った関数に突っ込んでよしなにですが……作業が一段落したらやってみます
— YSR@孤高の人間観察好き㌠ (@YSRKEN) 2016年2月27日
@YSRKEN はい
— yumetodo-C++erだけど化学科 (@yumetodo) 2016年2月27日
wifstreamのimubleにcodecvt指定する方法もあるのですが、bom skipが面倒になるのでおすすめしません。
@yumetodo まさかBOMに対応できないとは思いませんでしたもの
— YSR@孤高の人間観察好き㌠ (@YSRKEN) 2016年3月12日
@YSRKEN @yumetodo VC++のfopenにはBOM対応とかエンコーディング指定とかの拡張があるんですが、iostreamは構造上、拡張できませんからねぇ…。fopenで開いてからiostreamに渡すとかぐらいしか https://t.co/VRV02aat4X
— はぇ~☆ (@haxe) 2016年3月12日
@haxe @YSRKEN 別にKCS_KAIでやっているように、BOMskip書くのは全然難しくないですよ、istreamならね。なおwistreamは(ry
— yumetodo-C++erだけど化学科 (@yumetodo) 2016年3月13日
@yumetodo @YSRKEN 肝心なのはBOM skipだけでなく、UTF-16、UTF-8の自動デコードですよ。そんなのライブラリでやってほしいものです。
— はぇ~☆ (@haxe) 2016年3月13日
std::codecvt_utf8_utf16のtemplate引数の3番めに
— yumetodo-C++erだけど化学科 (@yumetodo) 2016年3月25日
std::codecvt_modeなるものが指定できて
std::consume_headerを指定するとBOMskipしてくれる!!!!! pic.twitter.com/ZvKMJj3DsC
いやぁ。これでUTF-8withBOMなファイルのBOMskipのためにわざわざ一回wstringstreamに読み込まなくてすむ!
— yumetodo-C++erだけど化学科 (@yumetodo) 2016年3月25日
いやぁ、素晴らしいね。これでBOMskip面倒だからと勧めづらかったWindows環境でのUTF-8withBOMなファイルのIOも薦められる!
— yumetodo-C++erだけど化学科 (@yumetodo) 2016年3月25日
祝!完全勝利!
— yumetodo-C++erだけど化学科 (@yumetodo) 2016年3月25日
バイナリエディタで見えるBOM(EFBBBF)がskipされて読み込めているのがわかる pic.twitter.com/IQwtXWfblv
追記: C++17時代での対処
std::codecvt_utf8_utf16
はdeprecatedになっているので、そもそもwchar_t系を利用しないで内部的にもUTF-8で扱うということでしょうね。そうすれば「これまで私がやってたbom skip」の項が使える。