10.5K Views
February 20, 23
スライド概要
C#使いに限りませんが、C++を「処理効率を少々犠牲にしてでも、バグを出しづらく安全に使いたい」という場合のポイントを説明しました。
2023/2/20の社内勉強会資料
C#使いのための 割と安全なC++ 2023/2/21 須藤(suusanex)
自己紹介 ID:suusanex( connpass・Twitter・GitHub共通) 名前:須藤圭太 サイエンスパーク株式会社という独立系ソフトウェアベンダーに所属 4年ほど受託開発で、上流から下流まで全部を回す ここ8年ほどは、自社製品開発も担当 Windowsアプリ開発のネタが多い 勉強会もやってます。 https://sciencepark.connpass.com
概要 C++はとても多様な書き方ができる言語 メモリを確保すれば、型もスコープも無視して効率よく使う事が出来る というより、そういう用途でこそ真価を発揮する しかし・・・ 普通のビジネスロジックをC++で書く場合、むしろその自由度は邪魔 その場合、自由度を減らして安全に書く方法を使おう (そういう部分はC#で書こう、が出来る案件ならその方が良いが・・・) 要素1つずつで勉強会が出来るようなものも多いので、今回は理解するための キーワードとその役割を説明するところまで
話のポイント メモリ:「メモリ等リソース解放漏れ」「バッファオーバーラン」を避ける技 Win32API:Windowsにおいては、C++そのものより「Win32APIとの組み合わせ 方」がポイント 特に、可変のメモリを引数にとるタイプ 例外:例外のメリットを得る (上級おまけ) STL:Linqの代わりにSTLが使える
メモリ:スコープでのリソース解放 スコープの外で使う必要が無いものは、スコープで解放されるように書く うっかり解放漏れを避けられるのも重要だが、何より「例外で抜けても解放さ れる」というメリットを得られる 主なテクニック ハンドル解放 コンテナ スマートポインタ
メモリ:ハンドル解放
ちょっとしたテクニックだが、知っているといないとでは効率が大違い
std::unique_ptrはテンプレートなので、HANDLE等のWin32APIの定義を追加す
ることが出来る
例:CloseHandleを自動で行う
次のように1回だけ定義
struct HandleDeleter {
void operator ()(const HANDLE handle) const
{
if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle);
}};
typedef std::unique_ptr<std::remove_pointer_t<HANDLE>, HandleDeleter> unique_HandleDeleter;
使い方↓
auto hFile = CreateFileW(略);
unique_HandleDeleter phFile(hFile);
メモリ:コンテナ ヒープメモリを自動で確保し、自分のデストラクタでメモリを解放してくれる ものの総称 とにかく、std::vectorを使えるようになろう BYTE*でできて、std::vectorで出来ないことは、何も無い 後のページのWin32APIとの組み合わせで、いくつか例示する std::vector<BYTE> buf; buf.push_back(3); BYTE* pBuf = buf.data(); //ここまでで実質的に次のコードと同じで、解放も自動 //auto* pBuf = new BYTE[1]; //pBuf[0] = 3;
メモリ:最低限のstd::vector さすがに、適当に使いすぎると効率が悪い push_back , assign, reserveあたりは知っておくべき ポイント:「確保しているメモリのサイズ」と、現在のsize()は異なる size()が確保メモリサイズを超えると、再確保するので遅い push_back:size()の範囲の末尾にデータ追加、size()を増やす assign:指定範囲にデータをコピー、それをsize()にする reserve:指定サイズメモリを確保させる。size()は変えない →その範囲でのpush_backはメモリ確保をしないので速い reserve()したメモリ 現在のsize() push_back() assign()
メモリ:スマートポインタ スコープを抜けた時に、newしたポインタを解放させたい場合に使う ローカル変数宣言でスタックメモリに置く場合は不要 しかしヒープメモリに確保する場合はこれが必須 newしたらstd::unique_ptr型のローカル変数に渡せ。これだけ。 const std::unique_ptr<SampleClass> pSampleClass(new SampleClass); pSampleClass->MemberFunc();
メモリ:キャスト かっこで囲むタイプのキャストは使わない(C-Style Castとして旧型扱い) (int)val 原則としてstatic_castを使う。間違えていたらコンパイラが教えてくれる static_cast<int>(val) reinterpret_castはほとんど使わない。 必要に見える場合の大半はキャストミス 通常、BYTE*バッファを構造体にキャストする場合くらいにしか使わないはず 「他にも使うケースがあるぞ」と言えるくらい詳しい人は自由にどうぞ
Win32API:データバッファのタイプ BYTE*などを引数に渡すタイプのAPI ローカル変数(スタックメモリ)で足りずヒープメモリを使う場合 new byte[]ではなく、vectorを使えば良い void SampleBufAPI(BYTE* pBuf, int bufSize); std::vector<BYTE> buf(128, 0); SampleBufAPI(buf.data(), static_cast<int>(buf.size()));
Win32API:文字列バッファのタイプ
wchar_tの配列を渡すと、そこに値を返すタイプのAPI
ローカル変数(スタックメモリ)で足りずヒープメモリを使う場合
vectorからwstringへの余計なコピーが発生するが、下のようにすれば解放漏れ
は無い
コピーを避けるのなら、C++17以降でbasic_string::data()を直接使えば良い
VC++前提なら、atlstr.hのCStringWでも良い
void SampleStrAPI(wchar_t* pBuf, int bufSizeCch);
std::vector<wchar_t> buf(MAX_PATH, L'\0');
SampleStrAPI(buf.data(), static_cast<int>(buf.size()));
std::wstring outputStr(buf.begin(), buf.end());
Win32API:ヘッダと可変データのタイプ 可変サイズバッファを作成し、そのヘッダ部に情報を書いてポインタを渡す、というタイプ 架空のAPIで例を示す。下記の定義で、例は次のページ struct SampleHeader { int BufferSize; BYTE Data[]; }; void SampleAPI(const SampleHeader* pBuf);
Win32API:ヘッダと可変データのタイプ
ポイントは、バッファに対するindexではなく、「buf.end()」というイテレータを基準に書き込んで
いること(index管理ミスがあり得ない)
これなら、解放漏れやオーバーランは起き得ない(キャストをミスらない限り)
BYTE inputData[3];//何らかの入力データがここに入っているイメージで読むこと
std::vector<BYTE> buf;
//必要なメモリサイズを計算して確保(処理速度向上のためで、必須ではない)
buf.reserve(sizeof(SampleHeader) + sizeof(inputData));
//ヘッダ部分を追加(0埋めで)
buf.resize(sizeof(SampleHeader), 0);
//データ部分を追加(すでに用意したデータをコピー)
buf.insert(buf.end(), &inputData[0], &inputData[3]);
//ヘッダ部分のポインタを取得し、ヘッダの構造体にキャストして値を書き込み
const auto pHeader = reinterpret_cast<SampleHeader*>(buf.data());
pHeader->BufferSize = static_cast<int>(buf.size());
//ヘッダ部分のポインタをAPIに渡す(bufが消えたり変更されない限りは安全)
SampleAPI(pHeader);
例外:例外のメリットを得る 例外のメリットは、めっちゃ大まかに言えば次の点 戻り値チェックがない分、コードがすっきりする(特に関数の階層が深い場合) 「エラーコード」以上のエラー情報を返しやすい(メモリ確保、全メソッドの共通 定義などの考慮不要) デメリットは次の点 「基本的に、エラーが出たらそこで処理中断」という処理方針の場合に限る 発生時の処理が非常に遅い→正常系では使えない つまり、本セッションの対象である「効率よりも、安全で保守性が高いコー ド」が目的ならば積極的に使うべき
例外:メリットを得るための条件 メリットを得るためには、下記のようなルールを全体で守る必要がある 注意:これはC++の必須ルールでは無く、本セッションの目的のためのルール 1,例外は「原則としてcatchしないもの」と理解する(あえて極端に言った) 例外が発生した場合の後処理を行うメソッドだけが、例外をcatchする 例:コンソールアプリで「例外発生時は、アプリケーション終了コードを返してア プリを終了する」という場合、catchするのはmainメソッドだけ 2,例外の型は、全てstd::exceptionを継承する こうすることで、最上位の関数でstd::exceptionをcatchすれば、必ず全ての例外を そこで止めることが出来る
(上級おまけ) STL:LINQの代わりに
STLが使える
list.FirstOrDefault(d => d.id == 3)みたいにシンプルに書けないのか?
書ける。ラムダ式も使えばほとんどLINQと同じ
std::vector<SampleIdClass> idList; //検索対象のリスト、実際にはデータが入っている
const auto result = std::ranges::find_if(idList,
[](const SampleIdClass& d) { return d.Id == 3; });
if(result != idList.end())
{
//対象が見つかった場合の処理
}
まとめ C++にも、安全優先の書き方ができる(速度は落ちるが) 「スコープを抜けたら解放されるように書く」が最大のポイント C++はC#と比べて厳密な定義を要求する傾向にあり、コードは複雑だが・・・ 同じ複雑なコードでも、解放漏れとバッファオーバーランを避けられるのは大 きい 覚えて積極的に使っていこう ただ、そういう場合はたいてい、C#を使った方がより良い(台無し)