CString::Formatの警告C6284を解消する方法|MFC/ATLの文字列(マルチバイト・Unicode切り替え)整理術

MFC/ATL のログ出力で CString::Format を使っていると、%s に CString をそのまま渡しただけで Visual Studio のコード解析から警告 C6284 が出て困ることがあります。原因は「実行時に動く/動かない」ではなく、「可変長引数(…)の型安全性」を解析器が厳しめに見ている点にあります。この記事では、C6284 の正体、最短での直し方、Unicode/マルチバイト切り替えの考え方、両対応の書き方、std::string との付き合い方まで、ログ周りの整理としてまとめます。

目次

CString::Format で警告 C6284 が出る理由(“動くのに怒られる” の正体)

CString::Format は printf 系と同じく「書式文字列 + 可変長引数(…)」の関数です。可変長引数はコンパイラが引数の型情報を保持できないため、解析ツールは「書式指定子が期待する型」と「渡している式の型」が完全に一致しているかを強くチェックします。

そして %s が期待するのは、あくまでヌル終端文字列へのポインタです(char* / wchar_t* / TCHAR* 相当)。一方で、CString はオブジェクトなので、そのまま渡すと解析器は「ポインタではなくオブジェクトが渡されている」と判断し、C6284 を出します。

CString strNewEntry;
CString strTempEntry = _T("something");

// 解析器が嫌がりやすい例(実行上は通っても警告になることがある)
strNewEntry.Format(_T("%s\r\n"), strTempEntry);

ここで重要なのは、_T マクロ自体が悪いわけではないことです。C6284 の核心は「%s に渡す型をポインタであると明示していない(ように見える)」点にあります。

最短で直す:%s には LPCTSTR を明示して渡す(C6284 の直接解消)

対処はシンプルで、CString から文字列ポインタ(LPCTSTR)を明示的に取り出して渡します。これで解析器に「ちゃんとポインタを渡している」と伝わり、C6284 が消えます。

よく使う 2 つの書き方

書き方例ポイント
LPCTSTR にキャストstrNewEntry.Format(_T("%s"), (LPCTSTR)strTempEntry);最短で警告を消しやすい。MFC では定番。
GetString() を使うstrNewEntry.Format(_T("%s"), strTempEntry.GetString());意図が明確で読みやすい。こちらを推す現場も多い。

質問例に沿って直すと、次のようになります。

// タイムスタンプ無し
strNewEntry.Format(_T("%s\r\n"), (LPCTSTR)strTempEntry);
// あるいは
strNewEntry.Format(_T("%s\r\n"), strTempEntry.GetString());
// タイムスタンプ有り(ミリ秒は 3 桁で十分なので %03d が自然)
strNewEntry.Format(
    _T("[%02d:%02d:%02d.%03d] %s\r\n"),
    stTimestamp.wHour,
    stTimestamp.wMinute,
    stTimestamp.wSecond,
    stTimestamp.wMilliseconds,
    (LPCTSTR)strTempEntry
);

なお、C++ らしく書きたい場合は C スタイルキャストではなく static_cast でも構いません。

strNewEntry.Format(_T("%s\r\n"), static_cast<LPCTSTR>(strTempEntry));

_T / TEXT マクロは原因ではない(役割を正しく理解する)

_T("...")(または TEXT("..."))は、プロジェクト設定に応じて文字列リテラルの型を切り替えるためのマクロです。つまり「Unicode/マルチバイト両対応」をしたいときに助けになる存在で、C6284 の原因ではありません。

プロジェクトの文字セット_T(“abc”) の実体TCHARCString の実体
Unicode 文字セットL"abc"wchar_tCStringW
マルチバイト文字セット"abc"charCStringA

つまり、_T は「将来 Unicode にしたい/環境で切り替えたい」というときの保険になります。一方で、今回の警告は「%s に渡すのがオブジェクトに見える」という解析上の問題なので、(LPCTSTR) や GetString() で解決する、という整理です。

Unicode/マルチバイトの切り替え方法(Visual Studio の設定)

「Unicode を使いたくない」「ANSI(マルチバイト)だけにしたい」という場合は、まずプロジェクト設定を固定するのが第一歩です。Visual Studio では次の設定が軸になります。

  • プロジェクトのプロパティ → 全般 → 文字セット
  • 「Unicode 文字セットを使用する」/「マルチバイト文字セットを使用する」

この設定により、UNICODE / _UNICODE の定義や、TCHAR 系 typedef の実体が切り替わります。ソース上で CString と書いていても、中身が CStringW か CStringA かが変わるのはここです。

「マルチバイト固定」で書く場合の方針

本当に「Unicode を一切使わない」と決めるなら、TCHAR 系に寄せるより、最初から型を明示して“ブレない”ようにするのも手です。

  • CStringA / LPCSTR / std::string を基本にする
  • Windows API は必要に応じて A 版(...A)を使う(例:CreateFileA)
  • 書式リテラルも _T を使わず "..." に統一する
CStringA line;
CStringA msg("hello");

// CStringA + Format(%s には LPCSTR を明示)
line.Format("[%s]\r\n", (LPCSTR)msg);

ただし実務では、Windows は内部的に UTF-16(Unicode)中心で進化してきた歴史があるため、A 版 API で通すほど「文字化け」「コードページ依存」「海外環境で再現しない」などのコストが出やすくなります。マルチバイトを選ぶなら、ログは ASCII 相当だけにする、あるいは入力が日本語を含むなら“どのコードページか”を設計に入れる、といった割り切りが必要です。

両対応(Unicode/マルチバイト切り替え可能)で書くときの基本形

同じソースを「Unicode でも MBCS でも」ビルドできるようにしたい場合は、CString / TCHAR / LPCTSTR / _T といった T 系を使うのが王道です。今回の C6284 も、この流儀で「ポインタを明示」すればスッキリします。

CString MakeLine(const CString& body)
{
    CString line;
    line.Format(_T("%s\r\n"), body.GetString()); // ここが重要
    return line;
}

STL の文字列も両対応したいなら、std::basic_string<TCHAR> の別名(エイリアス)を用意しておくと、プロジェクト設定に追従できます。

using tstring = std::basic_string<TCHAR>;

tstring s = _T("hello");

// tstring::c_str() は const TCHAR* になるので、%s と噛み合う
CString out;
out.Format(_T("%s"), s.c_str());

std::string / c_str() と CString::Format の関係(混ぜるならここを守る)

std::string / std::wstring はあくまで STL の“オブジェクト”なので、Format に渡すときは必ず c_str() でポインタを渡します。これは C6284 対策というより、printf 系の大原則です。

// マルチバイト固定の例
std::string test = "hello";
CStringA sHelp;
sHelp.Format("%s", test.c_str());

一方、Unicode ビルドの CString(= CStringW)に std::string(char列)を直接流し込むのは危険です。「見た目は同じ文字列」でも、文字コード体系が違うからです。どうしても混ぜるなら、変換の責務を明確にします。

変換が必要になる代表パターン

状況ありがちなミス安全な考え方
Unicode ビルドで std::string を %s に渡す%s が wide 前提なのに narrow を渡すTCHAR に合わせる(tstring へ寄せる/明示変換する)
MBCS ビルドで std::wstring を使うwide をそのまま %s に渡して崩れるMBCS 方針なら wide を持ち込まないか、境界で変換する
外部入力が UTF-8UTF-8 を ANSI と誤認してログが文字化け入力のエンコーディングを仕様化し、変換してから CString に乗せる

「ANSI だけで行く」と決めているなら、ログ周りも CStringA と std::string に統一し、境界(ファイル入出力、ネットワーク、UI)でだけ変換を行うのが事故を減らします。逆に「将来 Unicode にする可能性がある」なら、最初から CString / TCHAR / tstring を軸にして、char列は“必要な場所だけ”に閉じ込めた方が移行コストが小さくなります。

ログ生成コードを “警告が出ない形” に整える(実務向けのまとめ)

質問の Logging::AddEntry のような関数は、長期的には「フォーマットの責務」「改行付与」「タイムスタンプ」「スレッド安全性」あたりでコードが散らかりがちです。C6284 を直すついでに、ログ生成のパターンを固定するとメンテしやすくなります。

例:AppendFormat で段階的に組み立てる

Format は「全体を一回で作る」スタイルですが、ログは要素が増えやすいので AppendFormat も相性が良いです。

CString Logging::AddEntry(const CString& entry)
{
    SYSTEMTIME st{};
    ::GetLocalTime(&st);

    CString line;
    line.Preallocate(entry.GetLength() + 64); // ざっくり確保して断片化を減らす

    line.AppendFormat(_T("[%02u:%02u:%02u.%03u] "),
                      st.wHour, st.wMinute, st.wSecond, st.wMilliseconds);

    // %s に渡すのは必ず const TCHAR* を明示
    line.AppendFormat(_T("%s\r\n"), entry.GetString());

    return line;
}
  • const CString& で受けると不要なコピーが減ります(ログ呼び出し回数が多いと効きます)。
  • %03u のように、符号なしの型(WORD 等)には u 系を当てると、別の解析警告を避けやすくなります。
  • GetString() を挟むことで、C6284 のような「可変長引数の型推論」問題を最初から封じられます。

それでも C6284 が出る・再発するケース(チェックポイント)

C6284 は “%s と CString” だけの話ではなく、「書式指定子と実引数の型がズレている」全般で出ます。ログを触るときに一緒に潰しておくと、あとで痛い目を見にくくなります。

よくあるズレ例対策の方向性
64bit 値を %d に入れる__int64 v; Format(_T("%d"), v);型に合う指定子へ(環境に合わせて)
size_t を %d に入れるsize_t n; Format(_T("%d"), n);サイズ型は専用指定子を検討(方針を決めて統一)
符号付き/符号なし不一致WORD w; Format(_T("%d"), w);%u 系に寄せる、または明示キャスト
narrow/wide の混在Unicode ビルドで std::string を渡すTCHAR へ寄せるか、境界で変換して型を揃える

「警告がうるさいから無視」ではなく、ログは障害解析の最後の砦になりがちです。書式のズレは、たまたま動いていても、別環境・別ビルド・最適化条件で化ける可能性があります。C6284 を機に、Format 周りの型整合をルール化しておく価値は高いです。

警告を“消すだけ”の方法(非推奨だが知識として)

どうしても一時的に抑制したい場合、pragma で無効化できます。

#pragma warning(disable: 6284)
// ...
#pragma warning(default: 6284)

ただし、この方法は本当に危険な C6284 まで見えなくします。特に printf 系のズレはクラッシュや情報破壊の原因になるため、今回のように GetString() や (LPCTSTR) で意図を明示して直すのが基本です。

現場で事故らないための運用ルール(おすすめの落としどころ)

「今はマルチバイトで行く」「でも将来は分からない」「既存資産が MFC/ATL に寄っている」など、制約は現場ごとに違います。そこで、方針別に“揉めにくい”ルールをまとめます。

マルチバイト固定(ANSI)で行く場合

  • ログ生成は CStringA / LPCSTR / std::string に統一する
  • Format の %s には必ず (LPCSTR) / c_str() を渡す
  • 外部入力(UTF-8 等)の取り扱いを仕様化し、必要なら境界で変換する

両対応(Unicode/MBCS 切り替え)にしておく場合

  • 文字列は CString / TCHAR / LPCTSTR / _T で統一する
  • %s には GetString()(または明示キャスト)を必須ルールにする
  • STL は tstring = std::basic_string<TCHAR> を導入して足並みを揃える

どちらの方針でも共通して効くのは、「ログ関数の引数で受ける型を統一する」ことです。呼び出し側が std::string を渡したり CString を渡したりし始めると、境界が増えて変換ミスが起きやすくなります。ログは“入口を一つにしておく”だけで品質が上がります。

まとめ:C6284 の本質と、CString の扱いを一段整理する

  • C6284 の直接原因は「%s がポインタを期待しているのに、CString をそのまま渡しているように見える」こと。
  • (LPCTSTR) キャスト、または GetString() で明示的にポインタを渡せば解消する。
  • _T マクロは警告の原因ではなく、Unicode/マルチバイト切り替えを助けるためのもの。
  • std::string を渡すなら必ず c_str()。両対応したいなら std::basic_string<TCHAR>(tstring)で揃える。
  • ログは保守の要なので、C6284 を機に「書式指定子と型の整合」をルール化すると再発が減る。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次