32bit時代から保守しているコードをx64構成でビルドすると、C4311とC4312が出はじめます。原因は、たいてい64bitのポインタをDWORDのような32ビットの変数に入れている箇所です。C4311はポインタを入れている行に、C4312はその値をポインタへ戻している行に出ます。
変数の型を、ポインタと同じ幅のDWORD_PTRへ替えましょう。
C4311とC4312の読み分け
2つとも、幅の違う入れ物へ値を移したときに出ます。文面に出てくる型のどちらが大きいかで、直す側が決まります。
| 警告 | 日本語版の文面 | 起きていること | 直す場所 |
| C4311 | ‘変数’ : ポインターを ‘型’ から ‘型’ へ切り詰めます。 | ポインタを32ビットの入れ物へ入れている | 入れ物になっている変数の型 |
| C4312 | ‘操作’ : ‘type1’ からより大きいサイズの ‘type2’ へ変換します。 | 32ビットの値をポインタへ戻している | 値を持ってきた側の変数の型 |
どちらもコンパイラの警告(レベル1)です。C4312のページには「この警告は、64ビット コンパイル ターゲットに対してのみ発行されます」とあります。32bitのままビルドしている間は出ません。x64構成に切り替えた日にまとめて出るのは、このためです。
なぜx64で値が切れるのか
x64のポインタは64ビット、DWORDやintは32ビットです。C4311の説明にも、64ビットアーキテクチャ用にコンパイルされている場合、ポインタの値(64ビット)はint(32ビット)に代入されるときに切り詰められる、とあります。一度落ちた上位32ビットは、あとから広い型へ代入し直しても戻りません。
Microsoftのポインターの使用規則も、1つ目でポインタをint、long、ULONG、またはDWORDにキャストしないよう求めています。
同じx64構成では、整数の幅が合っていない箇所にも別の警告が出ます。size_tからサイズの小さな型へ変換している箇所がC4267、整数型をより小さい整数型へ変換している箇所がC4244です。どちらも整数どうしの縮小なので、直す型がC4311 / C4312とは違います。両方まとめて出ている場合は、C4267 / C4244の記事と読み分けてください。
手順1:ポインタを入れる変数をDWORD_PTRへ替える
その変数がアドレスを持つ可能性があるなら、ポインタ幅の型へ替えます。ビットのテストや設定のためにポインタをキャストする必要があるならUINT_PTRかINT_PTRです。どちらも32ビットと64ビットの両方のWindowsでポインタのサイズにスケーリングする整数型だと、使用規則に書かれています。
| 直す前 | 直したあと | 当たりやすい場所 |
| DWORD | DWORD_PTR | アイテムデータ、構造体メンバ、コールバックへ渡す値 |
| long / LONG | LONG_PTR | ウィンドウやコントロールへ預ける値 |
| UINT / ULONG | UINT_PTR / ULONG_PTR | ビット操作のためにアドレスを入れている一時変数 |
| int | INT_PTR | 戻り値でアドレスと整数を兼ねている関数 |
元の型がDWORDで、その値がポインタの幅である必要があるなら、DWORD_PTRへ変換します。変換の書き方はそのままで、入れ物の型だけを替えます。
// ポインタ精度の整数型。x64 では 8 バイトなので切り捨てが起きない。
const DWORD_PTR dwpStored = reinterpret_cast<DWORD_PTR>(pItem);
キャストを (DWORD) から (DWORD_PTR) へ書き換えても、受け取る変数がDWORDのままなら同じ位置で値が切れます。替えるのは変数の宣言です。LPARAM・WPARAM・LRESULTは64ビットへ広がる型なので、DWORDやlongの値と混ぜないでください。
手順2:ウィンドウに預けた値はSetWindowLongPtrで受け渡す
64ビットコンパイル時、Winuser.hはGWL_WNDPROC・GWL_HINSTANCE・GWL_HWNDPARENT・GWL_USERDATAを定義しません。代わりにGWLP_ の付いたインデックスが定義されます。ウィンドウやコントロールに自前のデータを紐づけている箇所は、コンパイルエラーとして手元に出てきます。
| 直す前 | 直したあと |
| SetWindowLong / GetWindowLong | SetWindowLongPtr / GetWindowLongPtr |
| GWL_USERDATA | GWLP_USERDATA |
| GWL_WNDPROC | GWLP_WNDPROC |
| (LONG) でのキャスト | (LONG_PTR) でのキャスト |
| cbWndExtraにsizeof(DWORD) | cbWndExtraにsizeof(DWORD_PTR) |
公式が挙げている例は、SetWindowLong(hWnd, GWL_WNDPROC, (LONG)MyWndProc); をSetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)MyWndProc); にする形です。ウィンドウクラスで追加領域を予約しているなら、WNDCLASSのcbWndExtraもsizeof(DWORD) からsizeof(DWORD_PTR) へ広げます。
GWLP_USERDATAの値は最初は0です。SetWindowLongPtrは成功すると指定したオフセットの前の値を返し、失敗すると0を返します。前の値が0で成功したときの戻り値も0なので、戻り値だけでは区別が付きません。SetLastErrorに0を渡してから呼び、戻り値が0でGetLastErrorが0以外なら失敗と見ます。
// 戻り値 0 は「前の値が 0 だった」ときにも返る。失敗と区別するため先に消しておく。
::SetLastError(0);
const LONG_PTR lpPrevious = ::SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(pItem));
const DWORD dwSetError = ::GetLastError();
読み戻しは同じインデックスで行います。受け取る変数もLONG_PTRです。
const LONG_PTR lpReadBack = ::GetWindowLongPtr(hWnd, GWLP_USERDATA);
他人のウィンドウの追加データを一時的に借りたときは、抜ける前に元の値へ戻します。この領域はウィンドウを作ったアプリケーションが使うためのもので、フレームワークや他のコードが先に使っていることがあります。
// 借りた場所は元の値へ戻す。ウィンドウは他の用途にも使われている。
::SetWindowLongPtr(hWnd, GWLP_USERDATA, lpPrevious);
手順3:リストの行はアイテムデータのまま受け取る
リストの行にオブジェクトのアドレスを紐づけている箇所は、MFC側の宣言を見ると直し方が分かります。CListCtrlの宣言はBOOL SetItemData(int nItem, DWORD_PTR dwData); とDWORD_PTR GetItemData(int nItem) const; で、どちらもDWORD_PTRです。扱う値も「32ビット (x64用にコンパイルする場合は64ビット) のアプリケーション固有の値」と説明されています。
GetItemDataの戻り値をDWORDの変数で受けると、そこで値が切れます。行に預ける前にアドレスをDWORDの変数へ通していれば、その行にC4311が出ます。受け取る変数をDWORD_PTRにすれば、キャストの形は変えずに済みます。
// SetItemData の引数は DWORD_PTR。API 側は x64 でも 8 バイトを受け取れる。
if (!pList->SetItemData(nRow, reinterpret_cast<DWORD_PTR>(pItem)))
{
result.m_sNote = _T("SetItemData が失敗した");
return result;
}
const DWORD_PTR dwpReadBack = pList->GetItemData(nRow);
手順4:32ビットへ切り捨てるならPtrToUlongを使う
幅を広げられない相手が残ることがあります。既存のファイル形式、通信プロトコル、引数が32ビット固定の外部SDKなどです。公式が用意しているのはPtrToLongとPtrToUlong(Basetsd.hで定義)で、呼び出しの間だけ切り捨ての警告を止めます。
// 公式が示す切り捨て手段。キャストと違って警告は出ないが、
// 落ちた上位 32 ビットが戻らないことは変わらない。
const DWORD dwNarrow = ::PtrToUlong(pItem);
ただし、これで変換した値をポインタとしてもう一度使ってはいけません。警告が消えても、落ちた上位32ビットは戻りません。上位32ビットだけが違う別のアドレスは、切り捨てると同じ値になります。相手の仕様で32ビットに落とすしかない場合も、値がぶつかって困らない用途に限って使います。
手順5:直したあとをx64で往復させて確かめる
型を替えたら、実行して値が戻るかを見ます。確かめたい経路の前後で、同じアドレスを0x%016llXの16桁で出力します。預ける直前のアドレス、入れ物の中の値、取り出して復元したアドレスの3つを並べます。
比べる経路は、DWORDへ入れる、DWORD_PTRへ入れる、GWLP_USERDATAへ預ける、リストのアイテムデータへ預けるの4通りです。x64のユーザーモードで上位32ビットが0でないアドレスが来ると、DWORDを通した経路だけ復元したアドレスが変わります。アドレスの値は実行のたびに変わることがあります。一致したかどうかと、落ちた上位32ビットの中身を控えておきます。

static_assert(sizeof(DWORD_PTR) == sizeof(void*)) を置いておくと、ポインタ幅で扱うつもりの型が本当にポインタ幅かを、実行する前に構成ごと確かめられます。復元したアドレスをポインタとして使い直すのは、元のアドレスと一致した経路だけにします。一致しなかった値は別の番地を指しているので、参照した先で何が起きるかは保証されません。
ここまでのコードはVisual Studio 2026 / Debug / x64 / Unicode / MFC共有DLLでビルドを通しています(/W4で警告0件)。警告のほうは、別に立てたプロジェクトで出しました。MFCを使わないコンソール アプリで、Visual Studio 2026 / Debug / x64 / /W4までは同じです。出た行がこれです。ファイル名は置き換え、行末のフルパスは省いています。
Legacy.cpp(14,9): warning C4311: '型キャスト': ポインターを 'const void *' から 'DWORD' へ切り詰めます。
Legacy.cpp(21,9): warning C4312: '型キャスト': 'DWORD' からより大きいサイズの 'const void *' へ変換します。
14行目が (DWORD) でポインタを入れている行、21行目がその値をポインタへ戻している行です。14行目にはC4302も並びます。表に載せた文面は公式ドキュメントの記載で、この2行は実際のビルドログです。
まとめ:64bit移行で直す順番
- 警告の文面に出ている2つの型を見る。C4311はポインタから32ビットへ、C4312は32ビットからポインタへ。幅が足りない側を広げる。
- 変数の型をDWORD_PTR / LONG_PTR / UINT_PTRへ替える。キャストの書き方だけを変えても、受け取る変数が32ビットのままなら値は切れたままになる。
- ウィンドウに預ける値はSetWindowLongPtrとGWLP_USERDATA。GWL_USERDATAは64ビットビルドで定義されないので、コンパイルエラーになった箇所がそのまま置き換えの対象になる。
- リストのアイテムデータはSetItemData / GetItemDataがもともとDWORD_PTRなので、受け取る変数の型だけを合わせる。
64bit化したアプリがデータベースにつながらなくなったときの切り分けは、ODBCのIM002エラーの記事にまとめています。
