「あ」という、たった1文字のstringを、wstringに変換したら、3文字になりました。しかも、中身はFFE3 FF81 FF82という、見覚えのない値です。変換した、つもりだったのですが。
std::wstring(s.begin(), s.end())は、stringからwstringを作る、いちばん手軽な書き方として、よく紹介されます。ところが、日本語を含む文字列には、使えません。正しい変換には、MultiByteToWideCharを使いますが、こちらも、渡すコードページと、文字列の中身のエンコーディングが合っていないと、別の形で壊れます。そこで、組み合わせを全部動かして、結果を並べました。
検証は、Visual Studio 2022付属のMSVC(19.44、x64、Release)で行いました。Windowsは、日本語環境(システムのコードページは932)です。
stringとwstringは、何が違うのか?
stringとwstringの違いは、
・stringはchar(1バイト)の並び、wstringはwchar_tの並び。Windowsでは、wchar_tは2バイトで、UTF-16として扱われる
です。ここで大事なのは、stringの中身が、どのエンコーディングなのかは、型からは分からないことです。同じ"こんにちは"でも、コンパイラの設定によって、UTF-8のバイト列にも、CP932(Shift_JIS)のバイト列にもなります。変換のときは、その違いを、自分で把握しておく必要があります。
実験①:wstring(s.begin(), s.end())で変換する
▼w3_naive.cpp
#include <cstdio>
#include <string>
int main()
{
const std::string utf8 = "あ"; // UTF-8 では 3バイト(E3 81 82)
const std::wstring expect = L"あ"; // 正解は、1文字(3042)
// よくある書き方: 1バイトずつ、そのままwchar_tに広げる
std::wstring w(utf8.begin(), utf8.end());
std::printf("元の文字列: %zu バイト\n", utf8.size());
std::printf("広げた結果: %zu 文字: ", w.size());
for (wchar_t c : w) std::printf("%04X ", (unsigned)c);
std::printf("\n");
std::printf("正解(wide): %zu 文字: %04X\n", expect.size(), (unsigned)expect[0]);
std::printf("w == 正解 ? %s\n", w == expect ? "true" : "false");
return 0;
}元の文字列: 3 バイト
広げた結果: 3 文字: FFE3 FF81 FF82
正解(wide): 1 文字: 3042
w == 正解 ? falseUTF-8の"あ"は3バイト(E3 81 82)ですが、変換結果も、3文字になりました。しかも、値はFFE3 FF81 FF82で、正解の3042とは、まったく違います。この書き方は、1バイトずつを、そのまま、1文字として広げているだけで、文字コードの変換は、一切していません。charが符号付きの環境(今回のMSVC)では、0xE3が負の値として扱われて、広げるときに、上位の桁がFFで埋められています。
この書き方で正しく動くのは、ASCII(英数字と記号)だけの文字列です。日本語を含むなら、使えません。
実験②:MultiByteToWideCharに、コードページを渡す
Windowsの標準的な変換は、MultiByteToWideCharです。どのエンコーディングとして読むかを、第1引数のコードページで指定します。CP_ACPは、システムの標準(今回は、CP932)、CP_UTF8は、UTF-8です。
2つのコードページ×2つの文字列の中身で、4通りを試しました。文字列は、同じ"こんにちは"ですが、コンパイル時の設定を変えて、中身のバイト列を、UTF-8とCP932の2通りにしました(/utf-8でUTF-8、/source-charset:utf-8 /execution-charset:shift_jisでCP932)。
▼w1_matrix.cpp
#include <windows.h>
#include <cstdio>
#include <string>
// 変換先のワイド文字列を、コードユニットの16進数で表示する
static std::string Hex(const std::wstring& w)
{
std::string s;
char buf[16];
for (wchar_t c : w)
{
std::snprintf(buf, sizeof(buf), "%04X ", (unsigned)c);
s += buf;
}
return s;
}
// narrow(char)文字列を、指定のコードページで、wideに変換する
static bool ToWide(const std::string& s, UINT cp, DWORD flags, std::wstring& out)
{
int len = MultiByteToWideChar(cp, flags, s.data(), (int)s.size(), nullptr, 0); // 必要な長さを聞く
if (len == 0) return false;
out.assign(len, L'\0');
MultiByteToWideChar(cp, flags, s.data(), (int)s.size(), &out[0], len);
return true;
}
int main()
{
const std::string narrow = "こんにちは"; // コンパイラの設定で、中身のバイト列が変わる
const std::wstring expect = L"こんにちは";
// narrowのバイト列
std::printf("narrowのバイト列: ");
for (unsigned char c : narrow) std::printf("%02X ", c);
std::printf("(%zu バイト)\n", narrow.size());
std::printf("正解のwide : %s\n\n", Hex(expect).c_str());
struct { const char* name; UINT cp; DWORD flags; } modes[] = {
{ "CP_ACP (フラグなし) ", CP_ACP, 0 },
{ "CP_ACP (MB_ERR_INVALID_CHARS)", CP_ACP, MB_ERR_INVALID_CHARS },
{ "CP_UTF8 (フラグなし) ", CP_UTF8, 0 },
{ "CP_UTF8 (MB_ERR_INVALID_CHARS)", CP_UTF8, MB_ERR_INVALID_CHARS },
};
for (auto& m : modes)
{
std::wstring w;
bool ok = ToWide(narrow, m.cp, m.flags, w);
if (!ok)
{
std::printf("%s -> 失敗 (GetLastError = %lu)\n", m.name, GetLastError());
}
else
{
std::printf("%s -> %2zu文字: %s%s\n", m.name, w.size(), Hex(w).c_str(), w == expect ? " [正しい]" : " [違う]");
}
}
return 0;
}narrowのバイト列: E3 81 93 E3 82 93 E3 81 AB E3 81 A1 E3 81 AF (15 バイト)
正解のwide : 3053 3093 306B 3061 306F
CP_ACP (フラグなし) -> 9文字: 7E3A 8599 FF53 7E3A FF6B 7E3A FF61 7E3A FF6F [違う]
CP_ACP (MB_ERR_INVALID_CHARS) -> 9文字: 7E3A 8599 FF53 7E3A FF6B 7E3A FF61 7E3A FF6F [違う]
CP_UTF8 (フラグなし) -> 5文字: 3053 3093 306B 3061 306F [正しい]
CP_UTF8 (MB_ERR_INVALID_CHARS) -> 5文字: 3053 3093 306B 3061 306F [正しい]narrowのバイト列: 82 B1 82 F1 82 C9 82 BF 82 CD (10 バイト)
正解のwide : 3053 3093 306B 3061 306F
CP_ACP (フラグなし) -> 5文字: 3053 3093 306B 3061 306F [正しい]
CP_ACP (MB_ERR_INVALID_CHARS) -> 5文字: 3053 3093 306B 3061 306F [正しい]
CP_UTF8 (フラグなし) -> 8文字: FFFD FFFD FFFD FFFD 0242 FFFD FFFD FFFD [違う]
CP_UTF8 (MB_ERR_INVALID_CHARS) -> 失敗 (GetLastError = 1113)| 文字列の中身 | コードページ | 結果 |
|---|---|---|
| UTF-8(15バイト) | CP_ACP(CP932として読む) | 9文字の、別の文字(7E3A 8599 …。画面に出すと「縺薙s…」のような形)。失敗にはならない |
| UTF-8(15バイト) | CP_UTF8 | 正しい(5文字) |
| CP932(10バイト) | CP_ACP | 正しい(5文字) |
| CP932(10バイト) | CP_UTF8 | FFFD(置換文字)だらけ。MB_ERR_INVALID_CHARSを付けると、失敗(エラー1113) |
組み合わせが合っているのは、2つだけです。見るべきは、組み合わせが合っていないときです。
- UTF-8の中身を、
CP_ACPで読むと、エラーにならず、別の文字に化けます。変換結果が9文字になっている(正解は5文字)ので、数が合わないことで、気づけます - CP932の中身を、
CP_UTF8で読むと、規則に合わないバイトが、FFFD(置換文字)に置き換えられます。MB_ERR_INVALID_CHARSを付けると、置き換えずに、失敗(エラー1113)を返すので、間違いに気づけます
MB_ERR_INVALID_CHARSは、今回の4通りでは、CP_UTF8で、不正な入力を検出してくれました。逆に、CP_ACPでは、UTF-8のバイト列を読んでも、失敗にならず、検出できませんでした。
実験③:長さに-1を渡す
MultiByteToWideCharの第3・第4引数に、-1を渡すと、「終端の\0まで」の長さを、自動で数えてくれます。便利ですが、戻り値に、終端の\0ぶんが、含まれます。
▼w2_len.cpp
#include <windows.h>
#include <cstdio>
#include <string>
int main()
{
const std::string utf8 = "こんにちは"; // /utf-8 でビルドするので、UTF-8
const std::wstring expect = L"こんにちは";
// 悪い例: 長さに-1(終端の'\0'まで)を渡すと、戻り値に終端ぶんが含まれる
{
int len = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, nullptr, 0);
std::wstring w(len, L'\0');
MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, &w[0], len);
std::printf("[-1 を渡した] len = %d, w.size() = %zu, w == 正解 ? %s\n", len, w.size(), w == expect ? "true" : "false");
std::printf(" 末尾のコードユニット = %04X\n", (unsigned)w.back());
}
// 良い例: 文字列の長さを、明示する
{
int len = MultiByteToWideChar(CP_UTF8, 0, utf8.data(), (int)utf8.size(), nullptr, 0);
std::wstring w(len, L'\0');
MultiByteToWideChar(CP_UTF8, 0, utf8.data(), (int)utf8.size(), &w[0], len);
std::printf("[長さを渡した] len = %d, w.size() = %zu, w == 正解 ? %s\n", len, w.size(), w == expect ? "true" : "false");
}
return 0;
}[-1 を渡した] len = 6, w.size() = 6, w == 正解 ? false
末尾のコードユニット = 0000
[長さを渡した] len = 5, w.size() = 5, w == 正解 ? true-1を渡した場合の長さは、6でした(「こんにちは」の5文字+終端\0)。この長さで、wstringを作ると、末尾に、\0(0000)の入った、6文字の文字列になり、正解のL"こんにちは"とは、一致しません。画面に出すだけなら、気づきにくい違いです。文字列の長さ(size())を、明示して渡すと、5文字で、正解と一致しました。
直し方:関数にまとめて、往復を確かめる
文字列の中身を、UTF-8に決めて(ソースは/utf-8でビルド)、変換を、2つの関数にまとめました。失敗を検出するために、MB_ERR_INVALID_CHARSも付けています。
▼w4_round.cpp
#include <windows.h>
#include <cstdio>
#include <string>
static std::wstring Utf8ToWide(const std::string& s)
{
if (s.empty()) return std::wstring();
int len = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, s.data(), (int)s.size(), nullptr, 0);
if (len == 0) return std::wstring();
std::wstring w(len, L'\0');
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, s.data(), (int)s.size(), &w[0], len);
return w;
}
static std::string WideToUtf8(const std::wstring& w)
{
if (w.empty()) return std::string();
int len = WideCharToMultiByte(CP_UTF8, 0, w.data(), (int)w.size(), nullptr, 0, nullptr, nullptr);
if (len == 0) return std::string();
std::string s(len, '\0');
WideCharToMultiByte(CP_UTF8, 0, w.data(), (int)w.size(), &s[0], len, nullptr, nullptr);
return s;
}
int main()
{
// 絵文字(サロゲートペア)や、空の文字列も、往復できるか
const std::string cases[] = { "こんにちは", "Hello", "", "日本語ABC123", "😀" };
for (const std::string& s : cases)
{
std::wstring w = Utf8ToWide(s);
std::string back = WideToUtf8(w);
std::printf("元 %2zu バイト -> wide %zu コードユニット -> 戻した %2zu バイト 往復で一致 = %s\n",
s.size(), w.size(), back.size(), back == s ? "true" : "false");
}
return 0;
}元 15 バイト -> wide 5 コードユニット -> 戻した 15 バイト 往復で一致 = true
元 5 バイト -> wide 5 コードユニット -> 戻した 5 バイト 往復で一致 = true
元 0 バイト -> wide 0 コードユニット -> 戻した 0 バイト 往復で一致 = true
元 15 バイト -> wide 9 コードユニット -> 戻した 15 バイト 往復で一致 = true
元 4 バイト -> wide 2 コードユニット -> 戻した 4 バイト 往復で一致 = true日本語、英数字、空の文字列、混在、絵文字(😀)のすべてで、元に戻して、一致しました。絵文字は、UTF-8で4バイトのものが、UTF-16では、2つのコードユニット(サロゲートペア)になります。wstringのsize()が、文字の数と、一致しないことがあるのは、このためです。
文字列の変換で、よくある失敗例と対処法
①wstring(s.begin(), s.end())を、日本語に使う
実験①のとおり、文字コードの変換は、していません。英数字だけで動いたからと、使い回さないでください。
②文字列の中身が、何のエンコーディングか、把握していない
ソースファイルの文字コード、コンパイルオプション(/utf-8など)、ファイルから読んだデータ。どれが、stringの中身を決めているのかを、先に決めます。決めたうえで、変換のコードページを、合わせます。
③変換の失敗を、確かめない
MultiByteToWideCharは、失敗すると0を返します。MB_ERR_INVALID_CHARSを付けたうえで、戻り値を確かめないと、実験②のCP_UTF8の失敗に、気づけません。
④-1を渡して、末尾の④-1を渡して、末尾の\0を含めてしまう
を含めてしまう
\0を含めてしまう実験③のとおり、wstringのsize()が、1つ多くなります。比較や連結で、おかしな結果になる原因です。
注意点
wstring(s.begin(), s.end())は、1バイトずつ広げるだけで、日本語では壊れる(FFE3 FF81 FF82)stringの中身が、UTF-8なのか、CP932なのかは、型からは分からない。コンパイルの設定で決まる- UTF-8を
CP_ACPで読むと、失敗せずに、別の文字に化ける。結果の長さが、合わないことで気づける - CP932を
CP_UTF8で読むと、FFFDが混ざる。MB_ERR_INVALID_CHARSを付けると、失敗として検出できる(エラー1113) - 長さに
-1を渡すと、終端の\0が含まれる。長さを明示するのが、安全 - 今回の検証は、Windows(日本語環境、コードページ932)、MSVC 19.44(x64、Release)で行った。他の環境では、結果が変わりうる
この記事のポイント
stringからwstringへの変換には、MultiByteToWideChar(逆はWideCharToMultiByte)を使う- 文字列の中身と、コードページを、必ず、合わせる。UTF-8に決めるなら、
CP_UTF8 MB_ERR_INVALID_CHARSと、戻り値の確認で、間違いを検出する- 長さは、
-1ではなく、size()を渡す
「変換できた」と「正しく変換できた」は、別の話でした。文字数が、期待と合っているか。往復して、元に戻るか。この2点を確かめるだけでも、日本語の変換の失敗は、ほとんど見つけられます。


