C#使いのための割と安全なC++

6.9K Views

February 20, 23

スライド概要

C#使いに限りませんが、C++を「処理効率を少々犠牲にしてでも、バグを出しづらく安全に使いたい」という場合のポイントを説明しました。
2023/2/20の社内勉強会資料

profile-image

会社勤めのSE・プログラマです。個人としての情報発信も行っており、このアカウントはその用途で使用します。同一ID「suusanex」でGitHub・はてな等でも発信しています。

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

C#使いのための 割と安全なC++ 2023/2/21 須藤(suusanex)

2.

自己紹介  ID:suusanex( connpass・Twitter・GitHub共通)  名前:須藤圭太  サイエンスパーク株式会社という独立系ソフトウェアベンダーに所属  4年ほど受託開発で、上流から下流まで全部を回す  ここ8年ほどは、自社製品開発も担当  Windowsアプリ開発のネタが多い  勉強会もやってます。  https://sciencepark.connpass.com

3.

概要  C++はとても多様な書き方ができる言語  メモリを確保すれば、型もスコープも無視して効率よく使う事が出来る  というより、そういう用途でこそ真価を発揮する  しかし・・・  普通のビジネスロジックをC++で書く場合、むしろその自由度は邪魔  その場合、自由度を減らして安全に書く方法を使おう   (そういう部分はC#で書こう、が出来る案件ならその方が良いが・・・) 要素1つずつで勉強会が出来るようなものも多いので、今回は理解するための キーワードとその役割を説明するところまで

4.

話のポイント  メモリ:「メモリ等リソース解放漏れ」「バッファオーバーラン」を避ける技  Win32API:Windowsにおいては、C++そのものより「Win32APIとの組み合わせ 方」がポイント  特に、可変のメモリを引数にとるタイプ  例外:例外のメリットを得る  (上級おまけ) STL:Linqの代わりにSTLが使える

5.

メモリ:スコープでのリソース解放  スコープの外で使う必要が無いものは、スコープで解放されるように書く  うっかり解放漏れを避けられるのも重要だが、何より「例外で抜けても解放さ れる」というメリットを得られる  主なテクニック  ハンドル解放  コンテナ  スマートポインタ

6.
[beta]
メモリ:ハンドル解放


ちょっとしたテクニックだが、知っているといないとでは効率が大違い



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);

7.

メモリ:コンテナ  ヒープメモリを自動で確保し、自分のデストラクタでメモリを解放してくれる ものの総称  とにかく、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;

8.

メモリ:最低限のstd::vector  さすがに、適当に使いすぎると効率が悪い  push_back , assign, reserveあたりは知っておくべき  ポイント:「確保しているメモリのサイズ」と、現在のsize()は異なる size()が確保メモリサイズを超えると、再確保するので遅い  push_back:size()の範囲の末尾にデータ追加、size()を増やす  assign:指定範囲にデータをコピー、それをsize()にする  reserve:指定サイズメモリを確保させる。size()は変えない →その範囲でのpush_backはメモリ確保をしないので速い reserve()したメモリ 現在のsize() push_back() assign()

9.

メモリ:スマートポインタ  スコープを抜けた時に、newしたポインタを解放させたい場合に使う  ローカル変数宣言でスタックメモリに置く場合は不要  しかしヒープメモリに確保する場合はこれが必須  newしたらstd::unique_ptr型のローカル変数に渡せ。これだけ。 const std::unique_ptr<SampleClass> pSampleClass(new SampleClass); pSampleClass->MemberFunc();

10.

メモリ:キャスト  かっこで囲むタイプのキャストは使わない(C-Style Castとして旧型扱い)   (int)val 原則としてstatic_castを使う。間違えていたらコンパイラが教えてくれる  static_cast<int>(val)  reinterpret_castはほとんど使わない。  必要に見える場合の大半はキャストミス  通常、BYTE*バッファを構造体にキャストする場合くらいにしか使わないはず  「他にも使うケースがあるぞ」と言えるくらい詳しい人は自由にどうぞ

11.

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()));

12.
[beta]
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());

13.

Win32API:ヘッダと可変データのタイプ  可変サイズバッファを作成し、そのヘッダ部に情報を書いてポインタを渡す、というタイプ  架空のAPIで例を示す。下記の定義で、例は次のページ struct SampleHeader { int BufferSize; BYTE Data[]; }; void SampleAPI(const SampleHeader* pBuf);

14.
[beta]
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);

15.

例外:例外のメリットを得る  例外のメリットは、めっちゃ大まかに言えば次の点  戻り値チェックがない分、コードがすっきりする(特に関数の階層が深い場合)    「エラーコード」以上のエラー情報を返しやすい(メモリ確保、全メソッドの共通 定義などの考慮不要) デメリットは次の点   「基本的に、エラーが出たらそこで処理中断」という処理方針の場合に限る 発生時の処理が非常に遅い→正常系では使えない つまり、本セッションの対象である「効率よりも、安全で保守性が高いコー ド」が目的ならば積極的に使うべき

16.

例外:メリットを得るための条件  メリットを得るためには、下記のようなルールを全体で守る必要がある    注意:これはC++の必須ルールでは無く、本セッションの目的のためのルール 1,例外は「原則としてcatchしないもの」と理解する(あえて極端に言った)  例外が発生した場合の後処理を行うメソッドだけが、例外をcatchする  例:コンソールアプリで「例外発生時は、アプリケーション終了コードを返してア プリを終了する」という場合、catchするのはmainメソッドだけ 2,例外の型は、全てstd::exceptionを継承する  こうすることで、最上位の関数でstd::exceptionをcatchすれば、必ず全ての例外を そこで止めることが出来る

17.
[beta]
(上級おまけ) 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())
{
//対象が見つかった場合の処理
}

18.

まとめ  C++にも、安全優先の書き方ができる(速度は落ちるが)  「スコープを抜けたら解放されるように書く」が最大のポイント  C++はC#と比べて厳密な定義を要求する傾向にあり、コードは複雑だが・・・  同じ複雑なコードでも、解放漏れとバッファオーバーランを避けられるのは大 きい  覚えて積極的に使っていこう  ただ、そういう場合はたいてい、C#を使った方がより良い(台無し)