自分の勉強用C++メモ。
この記事に公開するコードは全て無料で自由に使えます。以下このコードの利用について
・個人用も商用も無料で利用可能。
・報告も著作表記も一切不要。
・複製/編集/改変は無制限に許可。
・この著作者は、利用の都合で発生した問題に対する責任を取らない。
///ーー このメモについて ーー///
// ・C.C++ 及び道中で学んだプログラムについての諸勉強内容を個人的にまとめたノート。
// ・前回 C# Unity を勉強した時に、メモがばらけたのが面倒だったのでこの1ファイルにまとめることにした。
// ・あくまでメモであり、このファイル全体を実行しようとしてもバグるので注意。切り取って使ってね。
/// 次やること:お盆まで
// ・非同期処理系やりたい https://qiita.com/tan-y/items/ae54153ec3eb42f80638
// → move に対応したスレッドプール
// →async/await 使ったコルーチン系の非同期処理
//
// ・googleTest の挙動知りたいかも、、
// ・DIコンテナライブラリ知りたいかも、、
//
// ・例外の挙動を確認するために試しに使ってみよう。→これでもどこまで破棄されるんだ?強制終了じゃなくてリカバリーしようとしたら途中から戻れたりするのか?ためそうか。そして、もう一つどのくらいまで破棄されるのか&メモリリークしないようにするリカバリー込みの例が欲しいかも。
// ・継承が生じてるクラスで、コンストラクタが生じる基準が知りてぇ。コンストラクタってオーバーライドされるん?親クラスの変数に小クラスのインスタンスの場合親クラスのコンストラクタは生じる?小クラスの変数に小クラスのインスタンスの場合、親クラスのコンストラクタは生じる?https://teratail.com/questions/78535 ← dynamic_cast してる例.これ上位のFooクラスにコンストラクタで生成時にAかBかを判別するやつ作って、それからstatic_castすればいいやん?
// ・代入演算子が初期化時に適応されるのか
// ・関数マクロの演算子の実行順が変になるのは、関数マクロ定義と同時に中身記述した時だけか、それとも関数を当てはめた時もなるのか否か
// ・void型の調べたやつ、次はなんかメモリ管理のやつを見ながらやるよ
// ・google coding style で有名で有益なコーディングスタイル知れるかも。
// ・要素数不明の配列ってなんぞや?
// ・オーバーライドとデストラクタ調べないと https://cpp-lang.sevendays-study.com/ex-day6.html
// ・ヒープ領域分割できるっぽい? https://jp-seemore.com/iot/26927/#toc10
// ・std::bad_allocの定義見てみて、なんか friend とか noexception(例外飛ばさないやつだっけ?) とか二重 const(ポインタだから?) コンストラクタを継承してる?のとかよくわからんの
// ・一旦全体を整形→続き
//
// ・てかよ、時間ねぇよ、ゲーム作りまくりたくね?まじで時間ないよ。↑ここまでを、8月の連休までに終わらせる → お盆の1週間で今後の人生について考える
//
// ・C++の発展の方読んでまとめ
// ・ゼロから学ぶC++ 7~
// ・網羅的に説明してるC++のサイト Programing Place Plus のやつ学ぶ
// ・めっちゃちゃんと入門として教えてくれてるサイトの入門と応用を https://theolizer.com/cpp-school-root/cpp-school/ https://theolizer.com/cpp-school-root/cpp-school2/
// ・最適化のブックマーク一通り読む
// ・コンパイルの展開読めるようになりてぇ、、。
/// コーディングチェック項目
// ・再利用性はある設計になってるか?循環参照/相互依存的な設計になっていないか?
// ・外部から隠蔽された安全な設計になっているか?
// ・外部から機能が分かりやすく、編集/拡張がしやすい設計になっているか?
// ・可読性は十分にあるか?
// ・バグ値発生の対処はしているか? 例外処理/assert/nullチェックはしてるか?
// ・メモリを浪費していないか? メモリ開放を行っているか? 配列は delete[] と [] を最後につけているか?
// ・CPUを浪費していないか? 適切なコレクション型を用いているか? 計算量は無駄に多くないか?
///ーー オブジェクト指向言語の、コーディングで最も重要視すべきもの ーー///
// オブジェクト指向言語は端的に言うと「クラス」がある言語。クラスと、クラスに関連する継承やらインターフェースやらの機能を用いて、機能の再利用性や隠蔽性などをコンセプトにした言語のこと。
// 下記がコーディングにおいて最重要な要素で、●が大まかにオブジェクト指向言語に特有な設計目標。各言語の色んな標準機能がこの目標をより達成しやすいように組まれたり改良されたりしている。
// ● 再利用性(機能独立性 & 依存関係の抽象←具体の一方向性)
// ● 利用者からの内部隠蔽性(利用時の誤操作/誤改変に対する安全性)
// ● 開発者からの拡張容易性
// ○ 可読性
// ○ 最適化
// ○ ユーザー体験(ゲーム/アプリ等のコンテンツの場合)
// ・再利用性
// 機能を独立させて部品として扱い、他の箇所でも流用できるようにして作業の重複を省く。また、より具体的で下位的な部品からより抽象的で上位的な部品に向けて依存関係の方向を限定することで、全体における一部が変更された時に、その影響を下層のものだけに抑えられる。
// 機能を適切に分割出来てなかったり、循環的/相互的な依存関係を作る設計をするとこれを損なうので注意。
// また特に、上位の部品が下位の部品を使いたくなった場合は、関数オブジェクト(ラムダ式)やインターフェースなどを使って機能の実装部分or選択部分を下位の部品に委ねることで、依存関係を逆転させられる(上位←下位の依存関係を保てる)ので、これを上手く用いる必要がある。
// ・利用者空の内部依存性
// 利用者の操作で内部を致命的に改変させないように、ユーザーからの見える部分や操作可能な部分を意図した箇所に限定し、その他を隠蔽する。これはセキュリティの安全性も高める。
// ・開発者からの拡張容易性
// 開発者や拡張者が機能を編集/拡張する際は、なるべく容易で分かりやすく行えるように設計して、修正と拡張を効率化する。
// ・可読性
// 誰が見ても分かりやすくすることで、コードの追加修正を行うときの効率を上げる。詳細は後述。
// ・最適化
// メモリやCPUなどのリソースを効率化することで、最大限のパフォーマンスを追及する。
// ・ユーザー体験
// コンテンツを作るコーディングであるなら、単に機能の実装をするだけではなく、「ユーザー目線の遊びやすさ、使いやすさ」を特に最重要視する必要がある。コーディングの設計や可読性や最適化はユーザー視点では全く触れないため、どんなにそれら部分で優れたコードを書いても、ユーザー体験部分で劣ったものになると、その時点でそのコンテンツがゴミになり下がるため。
///ーー C++ の注意点 ーー///
// CやC++などのコンパイラ言語は、Rなどのスクリプト言語と違い、一行ずつ実行しないためデバッグがしにくい一方で、一度に全てをビルドできるので、最適化に特化しており超高速に動く。
// しかし、コンパイルする環境に非常に依存しやすい。そのため、環境依存で普通では起きないようなバグや不具合(コンストラクタが発生しないバグとか)が発生する可能性があり、特にゲームプログラミングだと、ゲーム機器の種類によっても環境が異なるため、様々なバグの対応が必要になる可能性がある。
// また、C++ の特徴として煩雑すぎるし古すぎるというどうしようもない巨大なデメリットがある。煩雑すぎるあまり安全性が低く、また古すぎるあまり使っていいライブラリと使うべきではないライブラリの区別も難しくなる。
// 現状プログラミング界隈で C/C++ が最も高速なので、パフォーマンスが求められる環境では使わざるをえないが、つい最近出てきた Rust という言語はほぼ C/C++ と遜色ない実行速度を誇るうえ、書き方が簡易化されており且つ安全性が高くなっているらしい。
// 今自分は出来る範囲で高品質なゲーム作るために Unreal Engine を使うために C++ を勉強してるけど、正直 UEくらいのクオリティのゲームエンジンで Rust が使える様になったら、Rust を勉強しなおすべき。
// あと一応 C/C++ にしかできない機能として「ハードウェアリソースやシステム関数に直接アクセスできる」というものがあり、例えば Rustは LLVMにのっかっているので LLVMができないことはどうしようもないが、C++はコンピュータができることならなんでもできる。という特徴があるらしいが、正直ゲームプログラミングに関係ないので考慮しなくていい。
///ーー メモリ領域(RAM) ーー///
// RAMは基本どんな言語でもテキスト領域、静的領域、ヒープ領域、スタック領域の4つに分けられる。
// スタックとヒープは逆方向に動的にメモリ確保されて、それ以外は静的に確保される(スタックは最大サイズは予め決まっている)。
// +--------+
// | text |
// +--------+
// | data |
// +--------+
// | heap |
// | ↓→ |
// | |
// +--------+
// | ↑ |
// | stack |
// +--------+
//
// ・テキスト領域:コードが格納される。コンパイル時に確保されてプログラム終了まで解放されない。
// ・静的(データ)領域:静的領域に定義したものが格納される。コンパイル時に確保されてプログラム終了まで解放されない。サイズでかい。処理は早い(予め確保されてるからメモリを確保する動作がない)。常時メモリ容量を圧迫する。
// ・ヒープ領域:ユーザーが明示的にメモリを確保した時に格納される。明示的な指示で確保と解放が必要。サイズでかい。処理遅い。2次元的に使用され、フラグメンテーションを生じる。
// ・スタック領域:関数で使う値全般+αを保持する(関数内で定義された変数、引数、戻り値、一時オブジェクトなど)。処理フローに則り自動確保と自動解放される。関数なら呼び出し時に、一時オブジェクトなら計算時に自動で確保され、関数ならスコープ終了時に、一時オブジェクトなら直後のセミコロン;やスコープ終了時に自動で解放される。1次元的に使用され逆順で解放されていく。サイズ小さい。確保場所を見つけやすいので処理早い。最大サイズが決まってるので、それを超えるとプログラム強制終了する。
// メモリ容量に対する負荷について
// ・静的領域を使うと常時負荷が生じる
// ・スタックは上限がある
// ・ヒープは使用によってフラグメンテーションが生じる
// メモリ処理速度に対する負荷について
// ・そもそもメモリを確保しない方法(パラメータの変化のみで使いまわす/ムーブする)を取ると処理時間はゼロに近くなる
// ・スタックを使うと高速
// ・ヒープを使うと低速
// (ちな ROMはもはや ReadOnlyではなくただの読み書き可能なストレージなぜかROMっていう)
///ーー ヒープのフラグメンテーション ーー///
// ヒープ領域はメモリを2次元的に使用していくため、メモリ解放が適切でないと長時間使用に伴って使用できない細かい隙間が大量に生まれていく。
// メモリブロック(配列とかの連続したメモリの塊)を上手く管理して、以下のような隙間を作らない工夫が必要。
// 1. メモリ確保と逆順にメモリ解放することを徹底する。A, B、C の順にメモリ確保したら C, B, A の順で全て解放すれば断片化は全く生じない。
// 2. 事前に最大必要量でメモリを確保して、それを使い回す。色んなタイミングで必要になるたびに確保して解放して、、を繰り返すよりも断片化を生じにくい。
// 3. ある程度までメモリを解放せず、区切りが良い所でメモリ解放を一気に行う。
// 2 と 3 と合わせた例:オブジェクトの出現と消失が繰り返し起こるゲームだと、出現と消失のたびに確保と解放を行うのではなく、消失時に使用していたメモリブロックをパラメータだけリセットした空きブロックとして置いといて、新たに出現したものは空きブロックがあればそれを使い、なければ新たに確保する。そして、ステージが切り替わる画面暗転タイミングなどで今まで確保してきたメモリブロックを一気に解放すれば断片化が起きない。
// 4. 確保するメモリブロックサイズを統一する。確保するメモリブロックのサイズがバラバラなときに断片化は起こる。Aは 128 バイト、Bは 512 バイト、Cは 64 バイト、必要な場合、AとC を最大サイズである 512 バイトで確保するようにすれば断片化は起きない。無駄なメモリ空間が発生するが、トレードオフとして断片化を防げる(メモリに余裕がある時に行う)。
// 5. メモリブロックの寿命に合わせてメモリ確保場所を変える。寿命が短い(or 小さい)メモリブロックの確保と解放は断片化の原因になりやすい。シングルスレッドな処理でもリスク上がるし、マルチスレッドはやばい。一時的なメモリブロックはそれ専用のメモリ空間を作って、そこから確保すると断片化が発生しにくくなる。
// 6. 自動で隙間を埋めて整理してくれるメモリコンパクションを使う。特殊なメモリ管理が必要だし、処理負荷もめっちゃ重い。使っても対象モジュールの範囲や容量を最適化する必要あり。
///ーー C/C++ のビルド ーー///
// ビルドには4つの過程があり、道中でいくつかファイルが介在する。エラーを吐く場所によって、プロジェクト全体のどこが悪いのかがわかったりもする。
// 「ソースコードファイル→ ①プリプロセス→ 一時ファイル→ ②コンパイル→ アセンブリファイル→ ③アセンブル→ オブジェクトファイル→ ④リンク→ 実行ファイル」の順
// ①プリプロセス:#include とかマクロとかの # で始まる命令の実行
// ②コンパイル:プログラム言語と機械語の中間のアセンブリ言語のファイルを作る。各ファイルでC/C++の書き方を間違えてるとここでエラー吐く。変数や関数は宣言だけ見て実装は④リンク時にみる
// ③アセンブル:アセンブリファイルを機械語の01ファイル(オブジェクトファイル、バイナリファイル)に変換する
// ④リンク:③までは翻訳単位(1つのソースファイルとそれにインクルードされて展開されるヘッダファイルを合わせたもの)毎に対して個別にコンパイルされてきた(分割コンパイル)のを、1つのファイルにまとめる。②で保留した変数や関数の実装部分を探しだし、見つからなければここでエラー吐く。
// 大体は➁と④のどちらにエラーが生じるかで、エラーの診断する。
// ビルドには Debugビルドと Releaseビルドがある。
// Debugビルドは重いし処理も遅いけど、printfとか assertとか使えるし、ログに色んな情報を載せてくれる。Releaseビルドは軽いけど最適化されててログとかデバッグ機能とか全然使えない。
// ファイルにして共有する時とか完成版とかは Releaseビルドにして、それ以外は Debugビルドにして使い分ける。
///ーー 可読性の向上 ーー///
// コードの可読性は、チームでも個人でもコードの追加修正を行うときの効率をめちゃくちゃ左右するので、誰が見ても分かりやすくする必要がある。
// コメントは頻繁に残して意図を伝える。特に以下の時には必須
// ・関数定義の頭に、その関数の処理内容と、引数、返り値の説明を残す
// ・クラス・構造体・分かりにくい変数には説明を残す
// ・&& とか || 使って条件文が長い時は改行したり () つけながら各条件の説明コメント付ける。
// 見やすく意図がわかりやすいコード整形をする
// ・見やすくするために改行を沢山挟むことをいとわない
// ・同じ内容を繰り返す時はコピペせずに関数を使う
// ・処理内容を区切るのためにブロックスコープを用いて { } 各処理文が何をしようとしているのかをコメントで記す。
// ・assert() 等を使ってコードを記述した時に想定した意図を伝える。特にメモリ確保確認と、関数定義初めの引数の値範囲確認、計算後の値の範囲確認は必須。
// ・if や for などで単文表記したり { } 省略しない、バグの温床だしデバッグできないし見にくいしで、メリットない。
// ・if 文で else あるなら、必ず else を先頭にする。} else { みたいにしない、else 見つけにくい。
// ・スタックメモリが足りない時以外は、なるべく関数の引数の中に返り値がある関数をそのままぶち込まない。()が多重になって見にくい、ローカル変数に格納した方が読みやすい。
// ・main() はなるべく短くして、中の関数の詳細を順番に説明するようにコードを書いていく。
// ・普通のコメントの時は // の後にスペースを設ける。コードのコメントアウトの時は // の後にスペースを付けない。 //printf("テスト用ログ出力");
// ・/* */ のコメントアウトは非推奨。後ろの */ を書き忘れると次の /* */ までコメント化されてバグりやすくなる。
// ・{}のネストは深くしすぎない。多くても6段ぐらいにする
// 命名規則
// ・大前提として、チーム制作時はチームの命名記法に則る必要がある。また、他人の管理しているコードを修正する時はその人の書き方に則るべき。途中で書き方が変わると読みにくいしその人が怒る。
// ・一般的でない or 対象が多すぎる略語を使わない:NG→ int dtn; 、OK→ int studentDataNum;
// ・ポインタ変数と参照変数は、頭文字に p / r をつける:NG→ int* dataNum; 、OK→ int* pDataNum;
// ・グロ変は、頭文字に g_ をつける:NG→ int allDataNum; 、OK→ int g_allDataNum;
// ・定数とマクロも、全部大文字で区切りはアンダーバーにする:NG→ const int peopleNum; 、OK→ const int PEOPLE_NUM;
// ・bool型とbool型返す関数は、肯定的な内容で、isやcanなどをつける:NG→ bool isNotEmpty; 、OK→ bool isSwitchOn;
// ・関数は、必ず動詞から始める:NG→ void ArraySwapFunction(){}、OK→ void SwapArray(){}
///ーー グローバルスコープ ーー///
// 静的領域に確保するので、プロジェクトが終了するまで常時メモリ負荷がかかる。(static と同じ)
// グローバル上では同名のものはプロジェクトにつき1つしか定義出来ないため、名前被りを避けるために namespace の使用と、長い名前にの命名が強く推奨される。
// グローバル変数の使用は「メモリが常時圧迫される」「生じたメモリ圧迫が、他の所から見つけにくい」「ファイル間の名前衝突」「スコープ広すぎて隠蔽性悪い」など問題が多く、使用はなるべく避けるべき。特にクラス内の staticメンバ変数で代替できる C++では使い所無い。
// またグローバル関数も staticメンバ関数で代替できる上、メンバ関数にした方がアクセス等を管理しやすいため、あまり使うことがない。
// 下手にグロ変使って常時メモリ圧迫される例
struct STUDENT {
int age, id, birth;
bool gender;
int score_math[200], score_lang[200], score_science[200], score_eng[200], score_society[200];
char name[100];
};
STUDENT g_studentArray[300000]; // まだこの変数使ってもないのに、常時 1.2GB くらいメモリが消えてる
// グロ変は他ファイルから使用する時は、使用先で extern を付けて再宣言する必要がある(ファイル間定義被りはNG判定されるのにそのままだと使用できない)。この時に再初期化は出来ない。
extern int g_otherFileValue; // int g_otherFileValue = 200; みたいな元の宣言や定義は他のファイルで行われてる。
// グローバルスコープのものに staticを付けると内部結合(同じ翻訳単位にあるものからしか認識できないこと ⇔ 外部結合)を保証でき、名前被りを避けられる。
static int g_otherFileValue; // int g_otherFileValue = 200; みたいな定義が他のファイルで行われててもOK。他ファイルからはこれは認識されない。
static void function() {} // 関数も static をつけるとそのファイル内だけにアクセスを制限できる。これは多用する。
///ーー satic 修飾子 ーー///
// グローバル以外の変数に static をつけると静的メモリ領域に確保されるようになる。
// static メンバ関数・メンバ変数
// クラス固有でインスタンス(オブジェクト)を必要としないし、同クラスでは共有でインスタンスごとに変化しない。多分 C# の static と似た感じ。
// staticメンバ変数は複数のクラスに継承して継承先から操作しても、継承元の static メンバ変数は単一のみで、全ての継承先でも値は共通になる。
// staticメンバ変数を使うタイミングとしては、クラスで共通して使う値があってインスタンス生成ごとにメモリを消費したくない時とか、コンストラクタとデストラクタで±1ずつとかしてインスタンスの現状数を数えるときとか、シングルトン作る時とか。「static メンバ変数・メンバ関数」にて詳細は後述。
// static ローカル変数
// 静的メモリに配置されるけど、関数内でしか使えないようにできる。ほぼ使わん。
// シングルトンクラスで、メンバ関数内で使用すると静的にインスタンスを生成するシングルトンになる( staticの代わりに new使って動的にも作れる、ただシングルトンより依存注入使お)。
// ただし、グロ変数/関数や staticメンバはテストを行う際に非常に妨げになり、機能を実装した際にはテストは必要不可欠なため、これらの使用は避けるべきとされている。
///ーー ファイル分け・インクルード ーー///
// ファイルは機能ごとに複数のファイルに分けると、可読性や管理性の向上だけでなくビルド(再コンパイル時)の時間の短縮になる。
// 例えば、10万行の1ファイルだけだと、そのうちの1行を編集するだけで10万行分の再コンパイルが生じるが、5百行の200ファイルだと、その1/200の再コンパイル時間になって作業効率が上がる。
#include <iostream>
#include <array>
#include <stdio.h>
#include <string.h>
#include <vector>
#include <map>
#include <algorithm>
#include <assert.h>
#include <limits>
#include <thread>
#include <mutex>
#include <iomanip>
#include <memory>
#include <concepts>
#include <functional>
#include <future>
#include <atomic>
#include <condition_variable>
#include <sstream>
#include <cstdint>
#include <queue>
#include <type_traits>
// インクルードは、インクルードしたファイルに、インクルードされたファイルの中身を全て展開する機能。
// インクルードされるファイルをヘッダファイルと言い、ヘッダファイル内では原則使う機能の宣言だけをする。
// ヘッダファイル以外のソースコードファイルをソースファイルといい、ソースファイル内では、使う機能の実装と、実際に機能の使用を行う。
// またソースファイル内で機能が実装されていなくても、同じヘッダファイルをインクルードしている、別のソースファイル内で機能が実装されていれば、その機能を使うことができる。
// 特定の機能を、同じヘッダファイルをインクルードした複数のソースファイル先で共通して使えるのがこの機能の利点。
// 欠点としては、インクルードが多いと展開処理が増えてコンパイルが重くなることと、includeされているファイルに変更があった場合はインクルード先の広範囲で再コンパイルが走ること(通常、変更のないソースファイルはビルド時の再コンパイル対象に含まれないが、インクルードはヘッダの中身をそのまま展開するので、多くのソースファイルからインクルードされているヘッダを変更すると、インクルード先のソースファイルが一斉に再コンパイルされる。そのため、大規模プロジェクトでは開発効率の面でインクルードの数を意図的に減らす設計を取ることもある)。
// 入れ子的にインクルードを行うと、自身より上位のインクルードされたファイルを全て使えるようになる。しかし、入れ子の形に限らずヘッダがヘッダをインクルードすると依存関係が複雑になってファイル管理のリスクとコストが爆上がりするため、「ヘッダ内ではヘッダを includeしない」様にするべき。特に循環参照みたいにお互いのヘッダを includeし合うと無限にプリプロセスが終わらなくて詰む。
// また、ヘッダ内でヘッダをインクルードしてしまいがちな設計としてよくあるのは、クラス内のメンバ変数の型として他のクラスを使用する場合が多い。これを避けるには、使用する他クラスを定義しているヘッダを includeするのではなく、使用するクラスのプロトタイプ宣言だけをすることで ヘッダ内のヘッダインクルードを避けることができる。(またインクルードも減らせるためリビルドが軽量化)
// ただし、前方宣言をしただけだと内部の定義までは見れないため、そのクラス内の定義に触れる処理を記すとエラーになる。(っていろいろ言われてるけど、コンパイラによっては前方宣言だけでも色々見てくれることが割とありそう、なんかVSの環境では普通に前方宣言だけでサイズ取得できたし中身アクセスできちゃった)
// これだとヘッダ内でヘッダをインクルードしちゃう
//------------------ AClass.hpp
#include "BClass.hpp"
class AClass { Bclass bb; };
//------------------ BClass.hpp
class BClass {};
// 前方宣言するとインクルード要らんくなる
//------------------ AClass.hpp
class BClass;
class AClass { Bclass bb; };
//------------------ BClass.hpp
class BClass {};
// ヘッダファイルに分けるもの一覧 https://programming-place.net/ppp/contents/cpp2/main/header_file.html
// ・グロ関数の宣言:プロトタイプ宣言
// ・グロ変数の宣言: グロ変は定義が被るとNGなので、複数回呼ばれるヘッダ内では定義できない。ヘッダ内で externで宣言だけして、ソースで定義する。
// ・ジェネリックグロ関数の定義:ジェネリックは多重定義が許可されるので定義もヘッダに書く
// ・staticメンバ変数の宣言:グロ変数と同じく定義が複数あるとNGなので、どこかのソースで定義する。
// ・複数のファイルで使用するクラス(構造体)の定義:メンバ関数はプロトタイプ宣言のみ(inline化したい時か、ジェネリックメンバ関数の時だけ定義も書く)
// ・複数のファイルで使用する列挙型の定義
// ・複数のファイルで使用するマクロの定義
// ・constexpr変数の定義
// ・型の別名定義(typeof)
// ソースファイルに分けるもの
// ・グロ関数の定義
// ・グロ変数の定義
// ・staticメンバ変数の定義
// ・クラスのメンバ関数の定義
// ・1ファイル内で使用が完結している上記以外のものすべて
// ファイル分けの例(最後のまとめ)→ https://c-lang.sevendays-study.com/ex-day7.html
// ↑これまとめよう
// 関数などは、1つのプログラム上では1度しか定義してはいけないという ODR制限がある(宣言は何回でも可能)。
// しかし、inline キーワードを関数に付けると、関数のインライン展開のヒントだけでなく、定義(実装内容)が全く同一であれば、複数のファイルに定義を記述できる機能もつく。
// そのため、ヘッダに関数の定義を書くときは、複数のソースファイルからインクルードされることを想定するなら、 inlineキーワードを付ける必要がある。
// ただし、inline が要らない状況も割と多い。テンプレート関数や、クラス定義内に直接定義する時など、、ただし、分からないなら全部 inline付けても大丈夫ではある。
// また、ごく短いメンバ関数はヘッダー内に inlineキーワードを付けて定義も全て記載することで処理が軽くなる。https://qiita.com/agate-pris/items/1b29726935f0b6e75768#fn3
// ファイル分けの例
//---------------example.hpp(ヘッダファイル。hppがよくある拡張子)
// インクルードをする度にインクルード元のファイルがその場に展開されるため、定義の重複を防ぐために、プリプロセッサで1度のみに管理する。
// #ifndef EXAMPLE_HPP_INCLUDED ←プリプロセッサ:マクロが定義されていない時
// #define EXAMPLE_HPP_INCLUDED ←プリプロセッサ:マクロを定義
void method(); // ←宣言のみ
// #endif ←プリプロセッサの if を終了
//---------------example.cpp(ソースファイル:実装。ヘッダファイルと同名.cpp がよくある名前)
// #include "example.hpp"
void Method(){ /* ~なんか実装~ */ } // ←実装のみ
//---------------main.cpp(ソースファイル:使用。使用するインクルード先の他ソースファイル)
// #include "example.hpp"
int main() {
Method(); // ←実際に使う
}
//---------------
///ーー inline キーワードとインライン展開 ーー///
// inline キーワードを使うと、「インライン展開」と「複数のファイルでの定義」が可能になる。
// インライン展開
// 関数の宣言と定義の場所は基本的には離れた場所になることが多く、宣言した関数は毎回処理時に関数の定義を探しに行く手間が生じる。インライン展開とは、コンパイル時に関数の宣言した場所で定義が展開されるようになり、分かれてる定義先(ファイル先など)に定義を探しに行く過程を省いて処理を高速化できる機能。
// ただし、C++ のインライン展開にはいくつか条件があって、➀定義がごく短いコードであること、➁メンバ関数以外の場合は、inline キーワードを付けること、③メンバ関数の場合は、virtual関数でないand (クラス定義内で定義される or クラス定義外だと inline キーワードを付ける)こと、という条件が必要になる。
// ただ最近は、メンバ関数以外の関数(➁)はコンパイラが勝手にインライン展開の判断をするようになったため、メンバ関数以外には inline キーワードの使用が無意味になっており、メンバ関数の時(③)だけ意識すればOK。
//---------------inline.hpp
class Inline1{
void func() { 12 * 12; } // 十分に短い & virtual関数でない & クラス内でメンバ定義されている → インライン展開されて高速化
};
class Inline2{
void func() { // 長い → インライン展開されない
1 * 1;
2 * 2;
/*~ なんか長い処理~ */
200 * 200;
}
};
class Inline3{
virtual void func() { 12 * 12; } // virtual関数でない → インライン展開されない
};
class Inline4 {
void func() // クラス内でメンバ定義されていない → インライン展開されない
};
Inline4::func() { 12 * 12; }
class Inline5 {
void func() // クラス内でメンバ定義されていない → インライン展開されない
};
inline Inline5::func() { 12 * 12; } // 十分に短い & virtual関数でない & クラス内でメンバ定義されていないが inline キーワードついてる → インライン展開されて高速化
//---------------
// 複数のファイルでの定義
// C++には、「宣言は何回でもOKだが定義はプログラム中でただの一度しか出来ない」という ODR制限がある。
// 本来はこの制限が適用されるため(クラス内メンバ関数とかの例外はある)、ヘッダに関数などの定義を記載すると全てのインクルード先で毎回展開されて定義が重複し、この制限に反してエラーを生じる。
// ただし、inline キーワードを付けると、複数のファイルで同じ関数が定義されていても、その「定義内容が全く同じ」であれば、エラーを回避できる。
//---------------inline2.hpp
void func() { 12 * 12; } // 複数のインクルード先で展開 → 定義重複でエラー
inline func2() { 12 * 12; } // 定義重複しても inline キーワードついているし、インクルード先では全て全く同じ定義がされるのでOK
//---------------
///ーー 名前空間 ーー///
// 名前空間を使えば、命名された名前のスコープ管理ができるようになる。よくライブラリや、機能ごとにまとめられる。
// グローバルスコープならいつでも、「namespace 名前空間名 { } 」でブロック内に記述することで、その名前空間内での記述ができる
namespace Player {
constexpr int PLAYER_MAX_HP = 100;
static int CalPlayerHp() { return PLAYER_MAX_HP; }
}
// 同名の関数、そしてその関数内に同名の変数を定義しているが、上のは Player 名前空間内で定義されているのでバッティングしない。
static void CalPlayerHp() { const int PLAYER_MAX_HP = 200; }
int main() {
// なぜなら、名前空間内で命名された名前は、「名前空間名::名前空間内の名前」がグローバルスコープの名前になるため。
printf("%d", Player::PLAYER_MAX_HP);
printf("%d", Player::CalPlayerHp());
}
namespace Player { // 同じ名前空間であれば手間の「名前空間名::」は不要になる。
static void PrintMaxHp() { printf("%d", PLAYER_MAX_HP); }
}
// また、「using namespace 名前空間名;」をファイル内で記述しても、その名前空間に限り、手前の「名前空間名::」が不要になる。
// ただし、これを使うと名前のバッティングを防ぐための名前空間の意味があまりなくなるため、「よっぽど頻繁に使う & 使う機能の名前がユニーク なライブラリ等」にしか使うべきではない。特にヘッダには、適応される範囲が広すぎて危険性が爆増するので、頻繁に使うものであっても書くべきではない。
using namespace std; // std が最たる使用例。
using std::time; // また、「using 名前空間::関数名」で、特定の関数だけを指定して省略使用することも可能。
// 使用例
//---------------example2.hpp
// #ifndef EXAMPLE2_HPP_INCLUDED
// #define EXAMPLE2_HPP_INCLUDED
namespace kari {
void Method1();
void Method2();
}
// #endif
//---------------example2.cpp(ソースファイル:実装)
// #include "example2.hpp"
namespace kari {
void Method1() { /* ~なんか実装~ */ }
void Method2() { /* ~なんか実装~ */ }
}
//---------------main.cpp(ソースファイル:使用)
// #include "example2.hpp"
using namespace kari;
using std::time;
int main() {
kari::Method1(); // 完全指名で同名バッティングを気にせず呼び出し。
Method2(); // using namespace kari; のおかげで kari:: を省いた形。この main.cpp 内に Method2(){} 関数があるとバッティングする
std::time_t result = time(nullptr); // std::time を省略して time() で利用可能。
std::cout << std::asctime(std::localtime(&result)) << result << " seconds since the Epoch\n";
}
//---------------
// 無名名前空間
// 名前のない名前空間を使うと、内部結合を表せる(グローバルスコープの static と同じ)。
namespace { // namespace だけで名前なし
int KonoFileDake = 0;
}
///ーー 同名定義とスコープ・有効範囲 ーー///
// 同名のものが存在するときは、より内部のスコープで宣言された変数が指定される。ただし、変数の前に「::」を加えると一つ上のスコープのものを指定できる。
int sameName = 11;
void printX() { printf("%d\n", sameName); }
int main() {
printf("%d\n", sameName); // 11
int sameName = 22;
printf("%d\n", sameName); // 22
printX(); // 11
if (true) {
int sameName = 33;
printf("%d\n", sameName); // 33
}
printf("%d\n", sameName); // 22
printf("%d\n", ::sameName); // 11
printX(); // 11
}
///ーー マクロ ーー///
// 定義したマクロと同名の変数や関数が存在すると上書きされるため、ファイル内で名前が被らないようにする必要がある(マクロのスコープはファイル内)。
// 定数マクロを使えば、#ifによる切り替えによってコンパイルから特定部分を取り除ける(ビルドを軽くできる)。
// 変数名の定義だけして #ifdef を使う例もあるが、必ず boolean の値入れて #if を使う。
// #ifdef はタイプミスを検知しないし、名前で検知しているため、将来的な機能変更による修正で逐一名前変えないといけないのがだるい。(#if なら T/F値の切り替えで済む)
// あと、定数の代わりに使える時もあるけど、普通に型指定できるから定数を使った方がいい。
#define IS_DEBUG true
int main() {
#if IS_DEBUG
// ~ デバッグ時にしかビルドされない処理 ~
#endif
// 〜 普通時の処理 〜
}
// 関数マクロは、インライン化を目的に使うことが出来たし、複数の型の処理をまとめて記載できたけど、関数のインライン化は今コンパイラが自動でやってくれるし、複数の型の処理をまとめて記載するのはジェネリック関数が使えるので使うことの利点が特にない。それどころか、関数マクロで定義した関数は、四則演算系が普通の関数と挙動が異なることがあり、思わぬ不具合等につながるのでもはや使うべきではない。
// ただし、標準で用意されている機能の中ではマクロとして定義されている者たちがいるので、それら既存のマクロ定義を上書きするとき(例えば、既存マクロの「malloc」を新しいメモリ確保/解放関数を作って上書きするなど)くらいしか使わない。
#define SQUARE() ((x) * (x))
int main() {
int a = SQUARE(10);
printf("%d", a); // 100
}
int main()
{
///ーー 変数 ーー///
// 値をメモリに一時保存しておきたいときに変数を使う。全ての変数は宣言と同時にその型(と配列の個数)分メモリを確保する。(この保持されたメモリ領域を実態、インスタンス、オブジェクトと呼ぶ)
// 記憶域上では0/1のビットとして格納されており、ビットが何を表すかは型によって異なる。
int x = 5;
// 宣言時に初期化をせずにあとで値を代入することも可。値がない変数は使用時にエラー
int y;
y = 5;
x = y = 5; // 代入には連続しながらが可能
int a = 5, b = 5; // 初期化には不可能。区切る必要がある。
// auto で型推論もある
auto x = 5;
///ーー 定数(読み取り専用変数) ーー///
// 値の変更がだるすぎるのでソースコードに直接現れる数値(マジックナンバー)は、定数を使って絶対に避ける。
// また、定数化するとコンパイルや処理速度が早くなるので、値を変えない部分には積極的に使用した方が良い(オーバーライドの final 的な)
const int X_CONST = 5; // const の値は実行時に決定する。
constexpr Y_CONSTEXPR = 20; // constexpr はコンパイル時に ROMに定数化する。
X_CONST = 10; // エラー
// コンパイル時に既に値が確定できるものは const ではなく constexpr を使うべき。ROMを使うしコンパイル時に計算するため、メモリの節約にもなるし実行時の負荷が軽くできるから。
// つまり定数を使う時は、const は関数の引数などのコンパイル時に決定しない定数に使い、コンパイル時に決定しているものは constexpr を使う。
constexpr PI = 3.14;
int functionConst(const Car car) { // 引数を変えたくないので定数に、コンパイル時に決定するためconst。
int result = car.get_fuel() * 30;
return result;
}
// constexpr変数同士や、constexpr変数と基本型を組み合わせた演算の結果で初期化した変数も constexpr変数にできる。
// そのため、返り値を constexpr にした関数は、引数に constexpr変数を与え、constexprの制約を満たした簡単な演算で答えを返す時に利用する。一応、 constexpr関数には、引数に実行時にしか決まらない値を渡して結果をreturnすることもできるが、その場合は、その返り値で constexpr変数を初期化することはできない。
constexpr int functionConstExpr(const int pi){
return pi * pi;
}
// メンバ変数において、static const を static constexpr にできるんやろか?その時に使われる領域ってどこ?静的領域?ROM?
///ーー 整数型 ーー///
// 整数リテラルは2進法、8進法、10進法、16進法の4種類ある。また数字の大きさでint →(uint)→ long → ulongの順に勝手に型が解釈される。
// 整数リテラルは、変数に格納されると進数の種類に関わらずその変数の値は 10進法になる。
0b1011; // 2進法の11、2進法は最初に0bがつく(2進法はC++14から)
013; // 8進法の11、8進法は最初に0がつく
11; // 10進法の11
0xB; // 16進法の11、16進法は最初に0xがつく
// 整数型はバイト数が異なる種類がいくつかある(以下に示すのは標準的なサイズであるが、処理系によってはサイズがばらつく)
char ch = 1; // 1バイト分(0~2^8 か -2^7~2^7)1文字分の文字コードを格納するが、内部的には単なる1バイトの整数なので文字の格納以外にも使える。
short sh = 2; // 2バイト分(-2^15~2^15)
int in = 4; // 4バイト分(-2^31~2^31)
long lo = 8; // 8バイト分(-2^63~2^63)
// unsigned を付けると正の数しか表現できなくなる代わりに、倍大きな値が表現できる。signed を付けると正負両方の値が表現できる。デフォは signed。
unsigned char ch = 255;
unsigned int x = 4000000000;
// <limits>で使ってるコンパイラで表現できる整数型の値の範囲がわかる。
#include <limits> // コンパイラで表現できる整数・浮動小数点の値の範囲をテンプレートクラスで使える。
printf("このコンパイラでは char 型は%s", (numeric_limits<char>::min() == 0) ? "unsigned" : "signed");
printf("int 型の最大値は:%d", numeric_limits<int>::max()); // 結果は処理系に依存する
// ・・・以上が「昔の」整数型について。
// 上記の組み込み型では処理系によってサイズが違い、移植性や最適化で問題が生じる可能性があるため、実は現在では組み込み型の整数型は非推奨であり、それに代わる stdint 系を使うべきである。
//「std::int"N"_t」でちょうど N ビットの範囲を持つ整数
//「std::int_least"N"_t」で少なくても N ビットの範囲を持つ最もサイズの小さい整数
//「std::int_fast"N"_t」で少なくとも N ビットの範囲を持つ最速で動作する整数
// N は ビット数であり、8, 16, 32, 64 を選択でき、2^N 個の範囲の整数を表現できる。ビット数は表現できる範囲を表しており、サイズは処理系に依存する。
std::int8_t; // ちょうど 8 ビットの範囲を持つ整数
std::uint16_t; // uint とすることで、符号なしにできる。ちょうど 16 ビットの符号なし範囲を持つ整数。
std::uint_least32_t; // 少なくとも 32 ビットの符号なし範囲を持つ最もサイズの小さい整数。
std::int_fast64_t; // 少なくとも 64 ビットの範囲を持つ最速で動作する整数。
// 使い分けとしては、
//「std::int"N"_t」は処理系に依存しにくい、移植性の高いコードを書くときに使う。
//「std::int_least"N"_t」は、メモリサイズを最小にしたい時に使う。例えば、std::int_least16_t の時、処理系的に2バイト(16bit)の整数型がない時、それ以上の範囲を表現できる最もサイズが小さい整数型、つまり 4バイト(32bit)の int があれば、他のより大きいサイズの整数型ではなくそれが当てはめられるようになる。
//「std::int_fast"N"_t」は、メモリサイズを気にせずに処理速度を早めたい時に使う。例えば、std::int_fast32_t の時、64bit の CPUを用いている場合、相性的に4バイト(32bit)の intを使うよりも 8バイト(64bit)の longを使う方が、高速に動くため後者が当てはめられるようになる(もちろんサイズが大きいのでその分メモリは消費する)。
// ちなみに「using 新しい型名 = 既存の型名」で型名のエイリアスを作れる(typedef の新しいやつ)ので、名前長いこいつらはエイリアスを作るのが一般的
using uif_32 = std::uint_fast32_t;
using il_64 = std::uint_least64_t;
///ーー 浮動小数点 ーー///
// 浮動小数点型は float、double、long double の3種類。浮動小数点は精度の桁数が決まっており、右のものほど精度が高いが重くなる。基本他のは重いので float使う。
// また、浮動小数点リテラルはデフォルトでは double型で、floatの場合はf、long doubleの場合はlを末尾につける。
float f = 10.3493f;
double d = 204.098;
long double ld = 46567.8977687l;
#include <limits> // コンパイラで表現できる整数・浮動小数点の値の範囲をテンプレートクラスで使える。
printf("このコンパイラでは float 型は最小値%f最大値%fまで表現可能", numeric_limits<float>::min(), numeric_limits<float>::max());
printf("このコンパイラでは float 型の精度は%d進法で%d桁まで", numeric_limits<float>::radix, numeric_limits<float>::digits);
// 浮動小数点は演算を重ねるごとに誤差が蓄積していくので、演算を積み重ねる場合はなるべく整数値で処理したものを使う。
float sum1 = 0.0f;
int sum2 = 0;
for (int i = 0; i < 1000; i++)
{
sum1 += i / 1000.0f;
}
for (int i = 0; i < 1000; i++)
{
sum2 += i;
}
printf("%f", sum1);
printf("%f", sum2 / 1000.0f); // こっちの方が誤差が蓄積しない
// 同じく、浮動小数点は値の誤差が生じるため、条件指定は範囲で行う
if (sum1 == 499.5) { printf("演算が適切に終わりましたよ"); }
if (499.4 < sum1 && sum1 < 499.6) { printf("演算が適切に終わりましたよ"); }
// 小数点以下の n進数
// 小数点以下の位を持つ数値を、10進法以外の他の進数に変換する時は大体有限桁にならないので注意。例えば、2進数の 0.1 は 10進数の 0.5、0.01は 0.25、0.01 は 0.125、、、という風に続いていくため、10 進数の小数点以下を 2進数で表現しようとしても、0.5, 0.25, 0.125、、、の和でない値は全て有限桁で表現できない。
///ーー 四則演算・符号・真偽値 ーー///
// int型の小数点以下は切り捨てられる
int x = 10 / 3; // 3
int y = (int)12.343f; // 12
double z = x / y; // 0。演算の結果は、最終的に当てはめる型ではなく演算中の型に依存する
double zz = x / 12.0; // 0.25。演算中の型がバラバラの時は、より表示範囲の広い方に暗黙キャストされる。
// C/C++ では累乗の計算は^ではできない。^はビットの排他的論理和らしい。
int res = 0;
for (int i = 0; i < 2; i++) { res *= x; } // forとか使うか
#include<math.h>
res = (int)pow(x, 2.0); // 引数と返り値が double型の pow関数を使う。
// 割り算系の「/」「%」は負の符号に対して使ってはダメ。処理系に依って結果がばらける
-22 / -5; // 4か5
-22 / 5; // -4か-5
22 / -5; // -4か-5
// 数字系の符号操作
printf("%f", +x); // 「+変数」でそのままの値、デフォがこれ
printf("%lf", -y); // 「-変数」で符号逆転の値
// インクリメント・デクリメント
// 前置の場合、その式が始まる前に±1される。
int n1 = 0;
int n2 = 0;
++n1; // n1は 1。
n2 = ++n2 * ++n2; // n2は 4。
// 後置の場合、その式が終わった後に±1される。
int n3 = 0;
int n4 = 0;
n3++; // n3は 0。この式のあと n3は1
n4 = n4++ * n4++; // n4は 0。この式のあと n4は2
// bool型は1バイトで数値と互換性がある。0は false、0以外の値は全て trueに変換される(整数値型と浮動小数点型との変換と同じ変換コンストラクタの機構)。
bool b = ture;
b = 0; // falseに変換される
b = -0.432; // tureに変換される
// 論理演算の短絡評価
// 連続的な論理演算は左から順に評価されていくが、その際に既に結果が分かっている場合は残りの評価をスキップする最適化が行われるため、より評価を左右しやすい駆るい処理から記載するべき。
bool Heavy1() { return false; }; bool Heavy2() { return ture; }; bool Light1() { return false; } // .... こんな感じで処理の重さが異なる複数の真偽値を返す処理。
if ((Heavy1() || Heavy2()) && Light1()) {} // この論理式の書き方だと最後の Light1() まですべての処理が行われる。
if (Light1() && (Heavy1() || Heavy2())) {} // この論理式の書き方だと最初の Light1() で結果が分かるため、以降の処理は省略される。
///ーー ビット演算 ーー///
// 複数の論理値(true/false)からなる集合同士を比較したりするのに超便利。あとは演算系でたまに使う
// << >> でシフトできる。ただし、負の数をシフトすると処理系依存で値ばらつくので行うべきではない。
int binary = 0b11011011;
binary << 3; // 11011000:3ビット左にシフト、右端から0が3つ分押し出す
binary >> 3; // 00011011:3ビット右にシフト、左端から0が3つ分押し出す
// 論理演算
0b101 & 0b100; // 100 両方真 (論理積)
0b101 | 0b100; // 101 片方でも真(論理和)
0b101 ^ 0b100; // 001 片方のみ真(論理差)
~0b101; // 010 真偽逆転
// 例えば、1~10のボタンがあり、そのうち 1,5,7,9のボタンがON、それ以外のボタンがOFF だと次の道が開かれるギミックがある時
bool is1on, is2on, is3on; // 、、こんな感じで10個管理するのはめんどい。
int trueButtonBinary = 0b1000101010; // 2進数で管理すると楽。
int imputNum = 0b0000000000;
/* ~ボタンによってフラグ変わる処理~ */
if ((trueButtonBinary ^ imputNum) == 0b0000000000) { /* ~道が開く処理~ */ }
// <limits>ヘッダにある numeric_limits<unsigned 整数値型>::digits で、その整数値型で表現できる2進法の桁数が分かる(桁数は処理系に依存する)。
printf("char 型の2進法の桁数は %d 桁", numeric_limits<unsigned char>::digits); // char型の2進法の桁数は8。
printf("int 型の2進法の桁数は %d 桁", numeric_limits<unsigned int>::digits); // char型の2進法の桁数は32。
// ちな std::bitset を使うと楽に2進法の操作できるよ
///ーー 配列 ーー///
// 配列は指定型の値を指定数分メモリ上に連続して確保する
// 初期化
int array1[] = { 0, 1, 2, 3, 4, 5 };
int array1[6] = {}; // 要素を全く指定しないとすべての要素がゼロで初期化。
// 多次元
int multiple[3][5]; // 縦横 3x5。右端に行くほどより細かい下位の階層になる。
multiple[1][2]; // 2行目3列
// 静的確保した配列のサイズは定数である必要がある。
int n = 5;
const int nn = 10;
#define NN 30
int array2[n]; // NG
int array2[nn]; // OK
int array2[NN]; // OK
// C言語だと配列の要素数を求めるメソッドがない。sizeof(配列名) で、配列の全体のメモリサイズがわかる。これとと1要素のメモリサイズで計算する。
int arrayNumber = sizeof(array1) / sizeof(array1[0]); // 配列の要素数 = 配列の要素全体の大きさ / 配列の1要素の大きさ
// ただし、配列をポインタとして宣言していた場合、sizeof(配列名)では、配列の全体のメモリサイズではなく、ポインタのサイズ(8バイト)になるため注意(sizeof(ポインタ) は参照先の値ではなく、普通にアドレスのサイズが返る & 配列はポインタとほぼ大差なく、配列名単体だと先頭の要素のアドレスになるから)。
sizeof(array1); // 4(int) x 6(要素数) = 24バイト
int* array3 = array1; // 配列名単体は先頭のアドレスを示すのでこれができる。
sizeof(array3); // array1、array3の先頭の要素のアドレスのサイズ = 8バイト
void func(int* arr) { sizeof(arr); }
func(array1); // 関数内部でポインタに代入されてるので、中の sizeof()は array1の先頭の要素のアドレスサイズ = 8バイトになる。
// これらのため、配列をポインタとして関数に渡したり動的確保したりする場合は、要素数が欲しいときは別の引数や変数として取っておく必要ある。
// C++の std::arrayを使えば、配列.size() で簡単に要素数が分かる。インクルードで #include <array> が必要
// 生配列と std::array の機能的な違いはほぼないから、C++では std::array の使用を推奨らしい。
std::array<int, 5> array4 = { {0, 1, 2, 3, 4} };
auto size = array4.size(); // 5
///ーー 文字・文字列 ーー///
// なんと文字リテラルは一時オブジェクトでもオブジェクトでもなく、静的領域に確保される。そのため、とんでも長い文字リテラルはテキストファイルで扱う(当たり前)。
'N'; // 文字リテラルは ''で挟む。空白は不可。
"HelloWorld."; // 文字列リテラルは ""ではさむ。空白は可能
// char型は1文字を表す型で、内部の値は整数値になっている(整数値としても利用可)。その整数値は特定の文字コード表の座標になっている(1辺16進数単位ずつ。例えば'Z'は右5上Aの5A、'1'は右3上1の31になり、'数'の文字でも内部の数値は全く別になる例えば'4'は58とか)ので、その値がなんの文字を表すのかはその環境で採用されてる文字コードに依存する。
char oneStr = 'N';
printf("%d", (int)oneStr); // 文字をint型とかの他の整数値型にキャストすると文字コード上の数値になる。
// C言語では文字列は char型の配列になる。配列は静的領域にある文字列リテラルの値をコピーした、新たなスタックメモリで作成される。
// 文字列の最後には、必ず¥0(NULL文字)が入り、文字数 + 1の配列サイズが必要になる。
char str1[64] = "HelloWorld."; // = {'H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd', '.' ,'\0'}; と同義、文字列だけ文字列リテラルを代入する書き方が可能。\0 含めた12の大きさが必要。
str1 = "後から文字列リテラル代入は、配列に後から配列全体を代入しようとするのと同じ"; // そのため、普通の配列と同じでこんなのは出来ない。
// 文字列は普通の配列と同じく、初期化せずに作るとめちゃくちゃな値が入るので必ず初期化する。
char newStr[64];
newStr[0] = '\0'; // 1文字目に空文字 '\0' を入れることで文字列をクリーンできる。
// 文字列は1文字ずつ入れ替えられるが、必ず最後に空文字 '¥0' が必要で、無いと使うときにエラー吐く。
for (int i = 0; i < 40; i++) {
newStr[i] = 'W';
}
newStr[40] = '\0'; // 文字列の最後を知らせるこれないとエラー吐く
cout << newStr; // 文字列なら配列でもこれで出力できる。
// ちなみに、文中にNULL文字があるとその時点までの文字列になる
newStr[0] = '1';
newStr[1] = '2';
newStr[2] = '\0';
newStr[3] = '3';
cout << newStr; // 12
// 文字リテラル " " による初期化や <string> で使えるようになる strcpy_s や strcat_s を使うと、自動で最後に '\0' を入れてくれる。
strcpy(str1, "Hey you crasy world"); // 右の文字列全体を左にコピー
strcat(newStr, str1); // 左の文字列の後ろに前の文字列を結合
// ポインタを用いた文字列は、静的領域に確保された文字列リテラルそのもの(の先頭アドレス)を指し、生配列と違いコピーをスタックメモリに生成しない
char* strPtr = "HelloWorld"; // これは、静的領域に確保された文字列リテラルの先頭アドレスを指す。
strPtr[0] = '\0'; // エラー。生配列と違い、静的領域上の値を指すので書き換え(要素の代入)は不可能。
strPtr = "Fly Away"; // これは大丈夫。"HelloWorld"とは別の静的領域に確保されている文字リテラルの先頭アドレスを指すように変わる(もちろん要素の書き換えは不可能)。
// この特徴があるので、生の文字列は動的確保を使用すると不具合を起こしやすく、また不具合が生じてもオーバーフローとかでバグの場所が分かりにくいので、使用するべきではない。
// C++の std::string を使えば、変数名.size() で簡単に文字数が分かる(挙動は std::vector とかなり近い、最後にNULL文字が入るくらいの違い)。インクルードで #include <string> が必要。
std::string str2 = "Hello";
str2[0]; // "H"
str2.size(); // 5
// std::string なら、+ 演算子で合体でき、== != で合致比較、< > で文字数の大きさ比較 も出来る
str2[0] + str2[4] + str2[4]; // "Hoo"
// こういう自動拡張する配列系はどのプログラミング言語でも総じて要素数を予め指定したほうがパフォーマンス良くなるのでした方がいい。
std::string str3;
str3.reserve(100); // string.reserve() でキャパシティを設定
str3 = "Prepare"; // 設定せずに自動でメモリ解放するよりも高速。
// char* による文字列と string による文字列の違いや最適化について色々言われてるやつ↓
// https://jp.quora.com/C-%E3%81%A7-char-%E3%82%92%E4%BD%BF%E3%81%86%E3%81%AE%E3%81%A8std-string-%E3%82%92%E4%BD%BF%E3%81%86%E3%81%AE%E3%81%A7%E3%81%AF-%E3%83%91%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%B3%E3%82%B9%E3%82%84%E3%83%AA
// ちなみに、環境によってはバックスラッシュや# などが表現出来ない場合があるが、そういったときは3つ組表示や2つ組表示という方法で代替表現できる。
"??/n"; // 「??/」でバックスラッシュを表せる。つまりこれは "\m" と同じ
///ーー ベクトル・マップ ーー///
// ベクトル
// C++ の機能。C言語だと自分で作る必要がある。Rのベクトル、C#のList と同じ感じで要素数の追加削除が可能。
// どのくらいのメモリを確保しなくてはならないか不明確時とかに便利。インクルードで #include <vector> が必要。
// 処理速度はもちろん生配列 or std::array の方が早い。
// また要素数変えれる動的配列あるあるで、先に要素数を決定して空配列を生成しておくと処理が早くなる(不足時に自動確保は重い)。
// これによく似た set は重複不可、list は前後追加可能、ただこれらはインデックス番号使えないからイテレータ必須。
std::vector<int> vec = { 0, 1, 2, 3, 4 };
auto size = vec.size(); // 5 ベクトル.size() で簡単に要素数が分かる
vec.emplace_back(5); // 末尾に要素追加
auto size2 = vec.size(); // 6
// マップ
// 辞書配列と同じ。std::map か std::unordered_map で可能。
// map, set はコンパイラが対応していれば unordered_map/unordered_multimap, unordered_set/unordered_multiset よりも常に劣る。
std::map<std::string, int> persons = { {"Alice", 18}, {"Bob", 20} };
persons["Alice"]; // 18
persons["Bob"]; // 20
persons.insert({ "Eve", 19 }); // 追加
persons.erase("Bob"); // 削除
// 他にも配列系色々ある。C++ STLのコンテナ型の特徴とパフォーマンスによる使い分けは → https://qiita.com/h_hiro_/items/a83a8fd2391d4a3f0e1c
///ーー イテレータ ーー///
// 配列とか vector とか辞書とか set とかの集合変数に対して、要素にアクセスする時に集合変数の種類に依存せずに(ジェネリックに)アクセスする。
// STLのコンテナ型だと共通して std::iterator 型の変数に格納できて、std::begin() や std::end() で取得、±整数値 で操作できる。
// ちなみに、イテレータを使うと set や list みたいにインデックス番号がないやつにもインデックスが使える。
std::vector<int> vect = { 0, 1, 2, 3, 4 };
std::array<int, 5> vect = { {0, 1, 2, 3, 4} }; // vectorでもarrayでもどちらでも下のコードが動く。
// std::count_if は条件を満たすコンテナ要素の個数を数える処理、第1引数と第2引数で範囲を指定、第3引数で関数オブジェクトで条件を指定
auto n = std::count_if(vect.begin(), vect.end(), [](const int v) { // 0より大きい2の倍数ならtrueのラムダ式
if (v <= 0) { return false; }
if (v % 2 != 0) { return false; }
return true;
});
std::cout << n << std::endl; // 2
// イテレータ型の変数を格納しても使える
vector<int>::iterator itr; // vector型のイテレータ 型指定するとジェネリックにはならなくなる
itr = vect.begin();
///ーー ポインタ変数 ーー///
// 値をメモリに一時保存しておきたいときに変数を使うが、全ての変数は宣言と同時にその型(と配列の個数)分メモリを確保する。(この保持されたメモリ領域を実態、インスタンス、オブジェクトと呼ぶ)
// メモリには保存場所を示すアドレス(16進数の整数値)があり、ポインタはそのアドレスを保持する変数。アドレスは型に依存せず8バイト(か 32bitPCだと4バイト)
// ポインタ変数はアドレス先にメモリを後から確保したり、*をつけて参照値を使ったりできる。
// 使うタイミングは、
// ・アドレス使うとき → ①ヒープに動的にメモリ確保したい時
// ・参照値使うとき → ①関数内で引数の編集したい時、 ②引数, 戻り値のサイズでかい時。
int x = 25;
// 宣言:「参照先の型 * 変数名」
int* intPointer = nullptr; // ポインタ変数は初期化を必ず行う。宣言だけして不定値が入った時に、たまたまその不定値が使用中のメモリを指すアドレスだったりすると不具合の原因になる。
intPointer = &x; // 「& 変数名」でその変数のアドレスを取得できる
// 宣言時以外は、*つけないとアドレス、*つけると参照値を返す。
std::cout << intPointer << std::endl; // 0x000000D6F20FFA84 とかそんなの
std::cout << *intPointer << std::endl; // 25
// 参照してるから、参照渡し的なノリになる。
*intPointer = 10;
std::cout << intPointer << std::endl; // 0x000000D6F20FFA84 とかそんなの
std::cout << &x << std::endl;
std::cout << *intPointer << std::endl; // 10
std::cout << x << std::endl; // 10
// ポインタ変数は空の値を入れたい時は、C言語なら NULL で、C++ では nullptr を使う
intPointer = nullptr;
// const で定数化
int x = 123;
const int* p = &x; // * の前に const を付けると、参照値を変更不可
*p = 456; // エラー
int* const p = &x; // * の後に const を付けると、アドレスを変更不可
p = nullptr; // エラー
const int* const p = &x; // 併用も可能。
// ダブルポインタ(アドレスのアドレス)
int x = 12; // int 型の4バイトのメモリを確保。
int* pPtr = &x; // 他の変数と同じく、ポインタ変数も宣言時にメモリを確保する(ポインタは型依存のメモリ数ではなく、アドレスの大きさの4か8バイトを固定で確保する)。
int** pDoublePtr = &pPtr; // ポインタ変数もメモリに確保されてるので、当然アドレスを持っていて、それをまた変数に格納できる。
int*** pTriplePtr = &pDoublePtr; // 理論上無限にアドレスを入れ子状に取得できるけど、ふつうダブル以上使わない。
// 普通のポインタ渡しと同じように使う
void func(int* ptr) {} // 普通のポインタ渡し
func(pPtr);
func(&x); // 参照値は内部でいじられても、スコープ抜けても残る。
void funcD(int** doublePtr) {} // ダブルポインタ渡し
funcD(pDoublePtr);
funcD(&pPtr); // ダブルポインタの参照値(つまりポインタ)はいじられても、スコープ抜けても残る。
// void 型ポインタ https://daeudaeu.com/c_void/#void-3
// void 型は何も無いという型。だが void 型の値は宣言も定義もできないが、void 型のポインタだけは色々使える。
void v; // エラー
void* ptr_v;
// void 型のポインタは、参照先の型を定めないポインタ(アドレスは4か8バイト固定なので参照先の型に依存せずに変数宣言時にとしてメモリ確保できる)。
// そのため void型ポインタの参照先の値は void型ポインタをキャストして型を確定させないと使えないし、 void型ポインタに対する±とかの演算は正しく動作しない。
// void型ポインタの使い所は2つ、①全ての型のポインタを扱える関数(主に引数と返り値)を作れる。②メモリアクセスをアドレスの管理のみに制限できる。
// ① キャストする時は元々入っているデータの型に合わせてキャストしないと値がバグる。ただしこれをしてもコンパイルエラーにならないので要注意。
char a;
a = 100;
ptr_v = &a;
printf("%d", *(int*)ptr_v); // これは元々1バイト分のデータしか持ってないのに、4バイト分読み込んでいるのでバグ値になる。
// free()とかmemset()とかのような使い方だと本当に何の型でも使えるが、結局参照値にアクセスするためにはキャストする必要があるので、使える型は絞られる。(だが、複数の型に対して一つの関数で対処ができるのは強い)
enum TYPE {
T_CHAR,
T_INT,
T_FLOAT
};
void PrintfData(void* data, TYPE type) { // 各型に応じてキャストと
switch (type) {
case T_CHAR:
printf("%c¥n", *(char*)data);
break;
case T_INT:
printf("%d¥n", *(int*)data);
break;
case T_FLOAT:
printf("%f¥n", *(float*)data);
break;
default:
break;
}
}
int main() {
PrintfData(&'わ', T_CHAR);
PrintfData(&12, T_INT);
PrintfData(&0.34, T_FLOAT);
}
// ②例
// NULL と nullptr (と NULL文字 '0')
// まず、C と C++ で NULLが表すものが違う。
#define NULL ((void*)0) // C
#define NULL 0 // C++
// そして nullptr は、アドレスが 0 であるvoid型のポインタとなっている。
// (そして NULL文字はただのビットフィールドが全て 0 のchar型の値)
// つまり、C の NULL と C++ の nullptr はアドレスが 0 であるvoid型ポインタ。C++ の NULL は値としての 0。(NULL文字は全ビットが 0 のchar型の値)
///ーー 配列とポインタ ーー///
// 配列はその要素数分メモリ上で連続して取得される。C/C++ の生配列は、配列全体をまとめて表すアドレスなどがなく配列の要素1個ごとにアドレスを操作するしかない。
int vals[] = { 4, 7, 11 };
int* valptr;
valptr = vals; // 配列を[添え字(番号)]無しで使うと、配列の先頭の要素を指すアドレスになる。
valptr = &vals[0]; // これと同義。
for (int i = 0; i < 5; i++) {
&vals; // &配列名 で普通の変数みたいにアドレスを取得しようとすると、配列の全ての要素に対するアドレス(ポインタ)の配列になる。int* ptr [5]; ← ここだとこんなの
printf("%d\n", &vals[i]);
}
int vals2[3];
vals = vals2; // また、配列名による配列の先頭アドレスは右辺値なので、こんな感じで代入したりはできない。これは & によるアドレス値と同じ。&a = ptr; 無理的な
// 生配列の要素にアクセスするには、配列名の後ろの[添え字(番号)]で可能。
// また、なんと配列のアドレスは「配列のアドレス +ー 整数値」 でその前後の要素のアクセスがになる。(「アドレス +- 要素の型のサイズ」でなくても良い!)
valptr; // 先頭アドレスが 1234567890 だとすると
valptr + 1; // このアドレスは 1234567891 ではなく、1要素サイズ分飛んだ 1234567894 になる。
*(valptr + 1); // 7 普通に値も次の要素の値。配列とポインタはかなり似てるけど、ココが大きな相違点。
// アドレスの取得、以下は同じ
std::cout << &vals[0] << endl;
std::cout << &valptr[0] << endl;
std::cout << vals << endl;
std::cout << valptr << endl;
std::cout << &vals[1] << endl;
std::cout << &valptr[1] << endl;
std::cout << vals + 1 << endl;
std::cout << valptr + 1 << endl;
// 参照値の取得、以下は同じ
std::cout << vals[0] << endl;
std::cout << valptr[0] << endl;
std::cout << *vals << endl;
std::cout << *valptr << endl;
std::cout << vals[1] << endl;
std::cout << valptr[1] << endl;
std::cout << *(vals + 1) << endl;
std::cout << *(valptr + 1) << endl;
// ちなみに文字列配列のポインタのみ、アドレスが文字列配列全体になる
double d[3] = { 1.1, 2.2, 3.3 };
double* pd = d;
char str[50] = "Hey you crazy world!";
char* pstr = str;
std::cout << pd << endl; // 000000B0BE0FF5F8 アドレス値
std::cout << *pd << endl; // 1.1
std::cout << pstr << endl; // Hey you crazy world! 文字列全体
std::cout << *pstr << endl; // H
// また、std::array や std::vector は「配列名」では配列全体の値、「配列名.data()」で先頭の要素を示すポインタ、「&配列名」で配列全体のポインタになるので注意。
int* first = array3.data();
std::array<int, 5> array4 = array3;
std::array<int, 5>* arrayPtr5 = &array3;
for (int i = 0; i < array3.size(); i++) { // こいつら全部同じ
printf("%d。\n", array3[i]);
printf("%d。\n", array4[i]);
printf("%d。\n", (*arrayPtr5)[i]);
}
// ポインタ型の引数は、ポインタ変数じゃなくてもアドレスを入れればOK。
void methodP(int* p1, int* p2, int* p3);
methodP(intPointer, &x, valptr);
// ポインタへの型変換は、ポインタ変数に代入するみたいにアドレスを使う。
int* px = (int*)&x;
char* ptr = (char*)str;
///ーー ポインタ変数・動的メモリ確保(ヒープメモリ) ーー///
// ポインタ変数に malloc関数・new演算子 等による右辺を代入すると、動的にヒープメモリを確保して、そのアドレスを取得できる
// 動的に確保したメモリは明示的に手動で解放しないと、プログラム終了まで残り続けメモリを圧迫する。
// メモリを動的に確保したポインタ変数は必ず「メモリが確保できたかの NULL チェック」と「使用後に 解放 した後、ポインタに nullptr を代入」が必要。これしないと確保失敗を認知できないのと、メモリリーク生じるのと、解放された領域に 解放 が再び行われた時にバグる。
// また、ヒープメモリ確保/解放の処理は重いので、forなどで繰り返すよりも、一度で大きな配列等を作って確保するべき。
// C と C++ でデフォで用意されている動的メモリ管理方法が違う
// C の仕組みで確保したメモリは、C の仕組みで解放する必要がある。これは C++ も同じ。new と free を組み合わせるのは不可能。
// デフォで用意されているものがあんま使えない時は、メモリ管理の関数を自作or演算子のオーバーロードしても良い。ただし、状況によってデフォのに戻したり自作のを使ったりと切り替える可能性があるので、作るとしても引数や返り値の型、数などはデフォに合わせるべき。関数自作の例(https://daeudaeu.com/memory_leak/)。newのオーバーロードの例は下の方に。
// また、グローバルスコープの new/deleteの演算子をオーバーロードすると、実はC++にある関数は内部でnew/deleteを使ってることがあるから、メモリリークがめっちゃ生じることがある(あった?)らしい。最近はなんかstd系でも使用するメモリを指定できる?か何かに変わってきてるらしい、、これ調べたほうがいいし、よく分からなかったらオーバーロードじゃなくて自作関数にしてマクロで定義の上書きしないか、クラススコープでのnew/deleteに限定しよう。
// C には、2つのメモリ確保関数(malloc, calloc)と、1つの確保済みメモリ領域をリサイズして移動させる関数(realloc)と、1つのメモリ解放関数(free)がある
// malloc関数
// size_t size は確保するメモリのバイト数。size_t は unsigned int とかが typedefで別名になってる。
// 返り値は、メモリ確保に成功するとvoid型の確保先のポインタを、失敗するとNULLが返る。
void* malloc(size_t size);
int* p1 = (int*)malloc(sizeof(int)); // よくあるのはこんなの。int型1個のサイズを調べて、返り値を(int*)型にキャストしてる。
Parent* p2 = (Parent*)malloc(sizeof(Parent) * 100); // 要素数100のParentクラス型の配列の場合。
assert(p1 != nullptr); // NULL ポインタのチェック、C なら NULL、C++ なら nullptr(NULLダメ)。
assert(p2 != nullptr); // NULL チェック
// calloc関数
// mallocとほとんど挙動一緒。第二引数は1個のメモリサイズ、第一引数はその数。返り値は一緒。
void* calloc(size_t nmemb, size_t size);
int* p1 = (int*)calloc(1, sizeof(int)); // 上と同じコードはこうなる。
Parent* p2 = (Parent*)calloc(100, sizeof(Parent));
assert(p1 != nullptr); // NULL チェック
assert(p2 != nullptr); // NULL チェック
// ただし、mallocは確保したメモリの値がバラバラなのに対して、callocは値が全ビットが0にクリアされた状態になる。
// realloc関数
// void* ptr はリサイズする対象のポインタで、NULLポインタの場合mallocと同じ挙動になる。size_t size はリサイズ後のメモリ領域の大きさ
// 返り値は、新しいメモリの確保に成功すると、前のメモリ領域を解放して新しい領域のvoid型ポインタを返す。失敗すると前のメモリ領域を解放せずにNULLを返す
// 前のメモリに格納されていた内容は、新しいメモリ領域に引き継がれる。新しい領域が前の領域より大きいと、引き継ぎで余った領域の値はバラバラになる。
void* realloc(void* ptr, size_t size);
// これら3つの size_t には 0 を入れてもエラーにはならないけど、メモリ確保が変になるし、アクセスしようとするとエラー生じるので避けるべき。
// free関数
// void* ptr は解放先のポインタで、NULLの時は何もしない、それ以外の時はポインタ先のメモリ領域を解放する。
void free(void* ptr);
free(p1); // 確保したやつの解放例
free(p2); // C++ と違って配列に対しても同じ記述をできる
p1 = nullptr; // 必ず解放後に NULLポインタを代入する。
p1 = nullptr;
// また、C++ コードの中で クラスを C のメモリ確保及び解放方法をとって操作しても、new/delete と違いコンストラクタとデストラクタは呼ばれない。
// つまり、C のヒープメモリ確保/解放方法をとると、callocによる全ビット0クリアの初期化でしか、ヒープメモリ操作と同時に値を操作できない。
// C++ には、メモリ確保と、メモリ解放の演算子が1つずつある。
// new演算子
// 右辺に newとポインタに代入する型を配置する。C と違って、サイズを指定する必要もないし、キャストする必要もない。
// またクラスを newすると、そのクラスでコンストラクタが定義されてる場合はコンストラクタを呼び出せる。
// またコンストラクタがあるクラス以外だと「new 型」の後ろに()で初期値を入れることで、初期値も代入できる(配列は{})。
int* p1 = new int;
Parent* p2 = new Parent[100]; // 配列はこうなる
Car* p3 = new Car(); // コンストラクタがあれば自動でそれが呼ばれる。()はあっても無くても動いたりするけど、分かりにくくなるし()つける
float* p4 = new float(3.453f); // 初期値も設定できる。
double* p5 = new double[3] {1, 2, 3}; // 配列の初期値設定
// 確保失敗の処理
// C と違い、newは確保に失敗すると NULLポインタを返さず例外 std::bad_alloc (<iostream.h>に入ってる)を throwする。
try
{
int* p1 = new int;
printf("例外が発生するとこのスコープ内のこれ以下は実行されない");
}
catch (bad_alloc)
{
printf("確保失敗");
abort(); // assert()の引数条件が tureの時に呼ばれるやつ。abort()は異常終了、exit()は正常終了。
}
// 例外はそれをキャッチする記述があるところまで関数の呼び出し元へ遡って処理されるので(どゆこと?)対処方法が共通である場合は毎回その場に try/catch を書く必要はない。 特にメモリの確保を失敗する状況では出来ることがほとんどない (メッセージを出して終了するくらい) ので main にひとつ書けば充分になることも多い。
void GetMemories() { /* 上記のメモリかくほする文全部 */ }
main()
{
try { GetMemories(); }
catch (bad_alloc) { printf("確保失敗"); }
}
// new 演算子の後ろに std::nothrow (<iostream.h>に入ってる)キーワードを()で用意すると C と同じように失敗時にNULLポインタを返すようになる
Parent* p2 = new (nothrow) Parent[100];
// delete演算子
// C の freeと同じで NULLポインタの時は何もしない、それ以外の時はポインタ先のメモリ領域を解放する
// 配列は delete に [] が必ず要る。ミスると実行時にエラー吐くが [] が無くてもコンパイルは普通に成功してしまうので「本当に」注意。
// クラスをdeleteすると、そのクラスで定義されているデストラクタを呼び出せる(主に内部で確保したヒープメモリの解放など)。
delete p1;
delete[] p2; // C と違って配列は [] が必要。
delete p3;
delete p4;
delete[] p5;
p1 = nullptr;
p2 = nullptr;
p3 = nullptr;
p4 = nullptr;
p5 = nullptr;
}
// ちなみに、動的確保で管理したい変数をメンバ変数にして、メモリの解放をクラス内のデストラクタで行う設計にすると、メモリリークを自動で防げる(RAII設計という)。
// ただし、クラスや構造体をデフォルトの機構でコピーやムーブすると、内部のメンバ変数をそのままコピーするため、ポインタがあると同じメモリを指すアドレスだけをコピーすることになる。
// そのため、よく使われるこの RAII設計を使うときには、コピー系やムーブ系を明示的に定義する必要があり、しないと deleteが同じメモリに対して重複してエラーになるので注意。
// コピー系(コピコンやコピ代入演算子)やムーブ系(ムブコンやムブ代入演算子)については後述。またこれを便利に行えるスマートポインタも後述してる。
// なんと Visual C++にはデバッグ時にメモリリークを自動検出してくれる _CrtSetDbgFlag関数が備わっている。
#include <crtdbg.h> // これをインクルードして、
// main() 関数内の先頭でフラグを立てることで Debugビルド環境に限りヒープメモリを色々チェックできる(Releseではスキップされる)。
int main() {
// 大体この2つのフラグが設定される。他にもあるから気になるなら調べて。
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | // デバッグ用のヒープメモリが使われるようになる。ランタイムはこのヒープメモリを検査することでメモリリークを検出する。
_CRTDBG_LEAK_CHECK_DF); // プログラム終了時に _CrtDumpMemoryLeaks を呼んで、メモリークをチェックし、リークしてればエラーメッセをデバッグウィンドウに出力する。
// エラーメッセ例
// Detected memory leaks!
// Dumping objects ->
// {80} normal block at 0x0000024D3910E310, 12 bytes long.
// Data: < > 01 00 00 00 02 00 00 00 03 00 00 00
// Object dump complete.
}
int main() {
// 多次元配列は、「1次元配列を使って疑似的に多次元配列にする」と メモリ開放の管理も処理時の負荷も軽くできる。
// 「ポインタを階層的にして作る」:二次元配列の例
int** arr = new (nothrow) int* [20]; // int型ポインタの配列を動的生成
assert(arr != nullptr);
for (int i = 0; i < 20; i++)
{
arr[i] = new (nothrow) int[10]; // 配列の要素にintの配列を動的生成して代入
assert(arr[i] != nullptr);
}
arr[19][9] = 12345; // 使用
for (int i = 0; i < 20; i++)
{
delete[] arr[i]; // 各要素を先にdelete
arr[i] = nullptr;
}
delete[] arr; // 最上位の配列を delete
arr = nullptr;
// 三次元だともう一段、四次元だともう二段、、、と次元の分だけ階層の delete が必要になってメモリ解放の管理がだるいのと要素数分new/deleteするのでその時にめっちゃ重くなる。
// 「一次元配列を使って疑似的に多次元配列にする」:二次元配列の例
int* arr2;
arr2 = new (nothrow) int[20 * 10]; // [20][10]の二次元配列を生成したと仮定
assert(arr2 != nullptr);
arr2[19 * 10 + 9] = 12345; // 二次元だと要素[X][Y]へのアクセスには[X*列数 + Y]で可能。三次元等も同じ感じでできる
delete[] arr2; // delete 一回でOK
arr2 = nullptr;
// メモリの解放の管理が最後の一発で良いし、new/deleteの回数も1回のいいので CPU負荷も軽い。
// また、一応要素の型がクラスとかになって重くなっても、ポインタを使えばメモリ負荷も多次元配列のものと変わらなくはできる(ポインタになるから CPU負荷軽くなる利点は消えるけど for文の入れ子と記載は少なく単純にできる)
}
// ちなみに、スタックで確保したものを動的に解放しようとするとバグる。普通はしないけど、スマポとかでミスって入れたりすることあるので注意。
int main() {
int x = 10;
std::unique_ptr<int> sp_int1(&x); // スマポに入れちゃう
} // デストラクタで自動的に解放しようとしてエラー。コンパイルは普通に通っちゃう。
// ここまで色々 malloc/free・new/delete によるメモリの確保と解放を記述してきたけど、スマートポインタがちゃんと実装された C++11以降では、make_unique()、make_shared() による確保と、スマートポインタによる自動解放と、reset()の自動解放を使ってメモリ確保と解放を行うべき。安全性や管理の点で完全に上位互換だから。(スマートポインタの詳細は後述)
int main(){
///ーー 参照変数 ーー///
// 参照値を保持する変数。ポインタの * が常時くっついててアドレス使えない版。使うタイミングはポインタからアドレスの機能を除いたのと同じ。C++ 特有でC にはない。
// 内部的にはまんまポインタと同じになっていて、参照変数はポインタ変数と同じだけメモリを食う(8バイトとか)。
// 存在意義は、ポインタと違って参照値のみにアクセスできるため、メモリの解放権(所有権)がなく、この変数から解放されないことを保証できる( & をつければ可能ではあるけどデフォでは無理)。
// 値に対してメモリに新しく複製せず、「同じメモリ先に別名をつけてアクセスする」というイメージに等しい。
int a = 50;
int b = 100;
int& rInt = a; // 宣言:「型 & 変数名」。初期化時に渡すのは変数である必要がある。
rInt = b;// 初期化時以降は参照先の値のみが変更され、変数を代入されても参照対象が変更されることはない。
std::cout << rInt << std::endl; // 100
rInt = 200;
std::cout << rInt << std::endl; // 200
std::cout << a << std::endl; // 200
// const 参照変数は色んな特徴がある(ポインタにはなく参照変数特有)。
const int& c = a; // const を付けると参照値が変更できなくなる。
const int& d = 13; // また参照値は変更できないが、左辺値でも右辺値でも参照できるようになる(変更可能な右辺値参照は && 使う。右辺値参照は後述)。
double& rDouble = a; // エラー。参照変数の初期化は、型が異なる値の場合暗黙的な型変換が出来る型だとしてもエラーになる(初期化でない代入の場合はOK)。
const double& e = a; // const を付けると、異なる型でも初期化できるようになる。
// ダブルポインタみたいに、ポインタ(アドレス)への参照値を保持もできる。
int f = 100;
int g = 200;
int* ptrF = &f;
int*& rPtrF = ptrF; // ポインタの参照値(ポインタを別名でアクセス可能に)
rPtrF = &g;
// &f == &g が等しくなる
// なんと参照変数を初期化する時には、コンストラクタ系だけでなく、代入演算子も呼び出されない。
// 本当に既存の変数に対してエイリアス(別名)を与えているだけということで、特別な処理になっているらしい。
///ーー 所有権(動的確保・ポインタ/参照) ーー///
// 動的に確保したメモリを解放できる権利。ポインタ変数は参照値へのアクセス権と解放権を持っており、参照変数は参照値へのアクセス権のみを持っている(参照変数も参照値から &でアドレス取得したら解放できるけど、生のままではできない。ちな普通の変数はコピー値へのアクセス権だけある)。つまり動的確保したメモリを扱う変数の内、ポインタ変数のみ所有権を持っていて、参照変数は所有権を持っていない。(正直名前の意味的に解放権か解放管理権にしてほしい、、。所有権ないって言うと参照変数がアクセス時に set出来ないみたいに聞こえる)
// 基本的に、所有権を持っているポインタは1インスタンスにつき1つだけのことが多い。ただし、たまに所有権を共有するインスタンスを意図的に作ったりする設計をとることもある。
// しかし、所有権を共有したくないのにコピーやムーブの過程で所有権を共有してしまうことが実は多々ある。これを防ぐためにコピー系やムーブ系を明示的に定義するか、スマートポインタを使う必要がある。
///ーー 条件分岐 (特に気になる所のみ) ーー///
// 条件の変数xには、整数型、列挙型、整数型か列挙型へ暗黙に変換できるクラス型 のみが可能
switch (x)
{
case 0:
// x == 0 のときの処理
break;
case 1:
// x == 1 のときの処理
break;
default:
// x がそれ以外のときの処理
break;
}
// break がないと、case判定関係なく下の文を連続で実行するので要注意!
// breakなしフォロースルーは分かりにくいので止めて、ちゃんと全てにbreakはつけた方がいい。
switch (x)
{
case 0:
printf("0"); // case0の時
case 1:
printf("1"); // case0の時とcase1の時
break;
case 2:
printf("2"); // case2の時
break;
default:
break;
}
///ーー 繰り返し構文 (特に気になる所のみ) ーー///
// for の()内の意味
{
int i = 0; // for の前で外に for{}内でしか使わないカウンタを宣言
for (i < 5) {
printf("%d", i);
i++; // for{}内の最後にカウンタを操作する処理
}
}
// ↑これを毎回するのだるい。
// for(「for の前で外に for{}スコープで有効な式」;「繰り返し条件」;「for{}内の最後に行う式」) で式を作れる。
for (int i = 0; i < 5; i++) {
printf("%d", i);
}
// for の範囲指定( = for each)。 for (要素の型 要素入れる変数 : 集合変数) { ~処理~ }
int nums[5] = { 10, 20, 30, 40, 50 };
for (int i : nums) {
printf("%d", i);
}
// 要素入れる変数は、デフォが値コピー、&を付けると参照、const を付けるとそれらを変更不可にできる。
for (const int& i : nums) {
printf("%d", i);
}
}
///ーー 関数 ーー///
// 宣言は呼び出し箇所(今回は main() )よりも前に無いと、コンパイル順でエラー吐く
void HelloWorld(int n) { // 命名は動詞から入るのが基本
for (int i = 0; i < n; ++i) {
std::cout << "[" << i << "] " << "Hello World!" << std::endl;
printf("[%d] Hello World!\n", i); // C言語と共通のこれと同義
}
char str[32];
int a = 2;
int b = 3;
int c = a + b;
sprintf(str, "The sum of %d and %d, so it's %d", a, b, c);// printf はコンソールに出力、sprintfはメモリに出力(つまり変数にコピー代入する)
printf("str is %s\n", str);
}
// main() よりも前でも、宣言だけ先にしていたら、main() の後に実装を書いてもOK(前方宣言)。
// ただし、関数名や引数を変更する時に前方宣言と定義を両方とも変える必要があったり、前方宣言を忘れたりするので、前方宣言して定義と分ける形ではなく、全て使用前に定義ごと書いた方が楽。
void KeyInput();
int main(){
HelloWorld(10);
KeyInput();
}
void KeyInput(){
int d, e;
cin >> d >> e; // cin(シーイン)はキーボードからの入力ストリーム >> は抽出子
std::cout << "a=" << d * e << endl;
scanf("%d", &d); // C言語と共通のこれと同義。
e = d;
printf("a=%d\n", d * e);
// ブロック文を使うと、関数内で任意のスコープを作れる。
{
char f[10];
scanf("%s", f, 10); // scanf_s()内の第2引数はポインタで、CやC++は文字列が配列になるので、配列名 になる。
printf("b = %s\n", f);
}
// 別スコープなので同名の変数を定義できる
{
char f[100];
scanf("%s", f, 100);
printf("b = %s\n", f);
}
}
// 同じ関数名でも、引数が異なれば複数定義出来る(関数のオーバーロード)
// 関数のオーバーロードを使うと、関数名を逐一沢山作らなくて済むが、逆に機能が大きく異なるのに同名にしてしまうと可読性が下がるので、あくまでオプションで機能を派生させる様な設計の時に使う。
void KeyInput(int n) {
int d;
cin >> d;
std::cout << "a=" << d * n << endl;
}
// ↓関数を定義する時は、こういう風に処理概要と引数、返り値が何を表してるのかの説明は必須↓
// 「Hello World の頭文字に1文字足したものを、指定回数コンソールに出力」
// int repeatNum :繰り返す回数
// char initialWord :頭文字に加える一文字
// 戻り値<int> :繰り返す回数に10を掛け合わした整数値
int HelloWorld(int repeatNum, int initialValue) {
// また、引数が取りえない異常値を取らないように検知&対処する設計も必要。
assert(repeatNum > 0); // 繰り返し回数が負だとエラー終了する例
};
// 再帰関数
// 必ず再帰を終了させる条件を書かないと無限ループになってバグる。
// また、抽象化されすぎていて可読性や理解性がかなり乏しくなるので、再帰関数にした方が効果的な時しか使うべきではない。
// main()関数
// main()関数はプログラムの実行を表しており、この内部で記述された処理がプログラム中に実行される。main()関数内で returnを実行するとその時点でプログラムは終了し、return 0 を返すと正常遂行、0以外を返すと不正終了となる。またretun 0 は記述しなくても、デフォルトで main()関数の最後に暗黙的に呼ばれる。
// また、main()関数には、コマンドライン引数という引数で、コマンドラインから実行した際の、コマンドライン上のパラメータを取得できる。
int main(int argc, char** argv) {
argc; // プログラム名とプログラム仮引数を合わせた個数
argy; // プログラム名とそれ以降のプログラム仮引数名の配列
}
///ーー 値渡し、ポインタ渡し、参照渡し ーー///
// 関数の引数外での、普通の変数、ポインタ変数、参照変数の扱いと同じ。
// 値渡しと参照渡しは、引数に値を渡す。ポインタ渡しはアドレスを渡す。
// 値渡しがそのままの値を使って、ポインタ渡しがアドレスと参照値を、参照渡しが参照値を使う。
// 値渡しだと引数が左辺値の時にコピーごとにメモリ食う&毎回コピコンが生じて負荷かかるので、参照値を渡す参照渡し(かポインタ渡し)にした方が良い。特に参照渡しは、参照変数を初期化する時と同じく代入演算子による処理も生じないので非常に高速になる。(なぜか値渡しの時には処理的に近いはずなのに引数が右辺値でもムブコンは生じず、std::move()変換しないとムブコンが生じない。そのままで生じるのは左辺値の時のコピコンのみ)
// また、参照渡しが使える C++ではポインタ渡しは使うべきではない。ポインタ渡しではポインタの中身が空かどうか分からないため必ず nullチェックが必要だが、参照はそもそも参照先が null だと代入時にエラーが生じるので null チェックが必要ないため、そのチェックコストやチェック忘れが無くなる。
// 値渡し、引数は普通の値。
void func(int x)
{
// x を変える
} // 値は戻る
// ポインタ渡し、引数はアドレス。
void func(int* x)
{
// x を変える
} // 値は戻る
void func(int* x)
{
// *x を変える
} // 値は戻らない
// 参照渡し、引数は普通の値。
void func(int& x)
{
// x を変える
} // 値は戻らない
// ダブルポインタ渡し。引数はダブルポインタ
void func(int** x)
{
// x を変える
}// 値は戻る
void func(int** x)
{
// *x を変える
}// 値は戻らない
void func(int** x)
{
// **x を変える
}// 値は戻らない
// ポインタの参照渡し。引数はポインタ
void func(int*& x)
{
// x を変える
}// 値はもどらない
void func(int*& x)
{
// *x を変える
}// 値はもどらない
// ポインタや参照変数扱うときと同じく、ポインタ渡しや参照渡しで引数に const をつけると、「参照元」を変化できないことを保証する。
void M1(const int* pInt) {
*pInt = 10; // NG。参照元は変更できない
pInt = nullptr; // OK。ポインタ自体は変更できる
};
// const 参照変数による効果は、参照渡しの引数に const を付けた時も同じく適用される(ポインタにはなく参照変数特有)
void M2(const int& rInt){}
int main() {
int i = 10;
double d = 10.39;
M2(i);
M2(10); // 右辺値も参照できるようになる。
M2(d); // 異なる型でも初期化できるようになる。
// この特徴を用いて、参照値も使いたいし定数も使いたい時などによく使われる。
}
///ーー 関数オブジェクト ーー///
// 関数を変数みたいに扱えるようにしたものを関数オブジェクトという(昔はファンクタのことを関数オブジェクトと呼んでいたらしい、色々調べると混同してるから気を付けて)。
// 関数オブジェクトには、関数をポインタの形にした関数ポインタ、関数を変数の形にして簡易的に記述出来るようにしたラムダ式、std::function など色々な種類がある
// コールバック関数つくれたり、ポリモーフィズム(同じ書き方で複数の処理に文化できる)を作れたりする。
// 関数ポインタ
// 関数をポインタ変数の形にしたもの(左辺値)で、戻り値の型と引数の型・数・順番が全て一致している関数のみが代入可能。
int Add(int x, int y) {
return x + y;
}
int main() {
int (*fp)(int, int); // 「戻り値の型 (*関数ポインタ名) (引数の型...); 」
fp = Add; // 関数 Add のアドレスを保持する関数ポインタ fp
int result = fp(3, 5); // 「関数ポインタ名(引数)」。関数の呼び出しみたいな形で 関数ポインタ fp を介して関数 Add が実行される
std::cout << result << std::endl; // 8
auto fp_noDefine = Add; // なんと auto で型推定すると独特でめんどくさい関数ポインタの宣言が必要なくなるので便利。
std::cout << fp_noDefine(3, 5) << std::endl;
}
// 関数ポインタの宣言は、int (*)(int, int) というかなり独特で分かりにくい形なので、一般的には関数ポインタを使うときには typedef を使って分かりやすい名前に変更する。
typedef void (*method_ptr_t)(void);
// 関数ポインタを配列として使うと switch case 等の条件分岐を必要としない、要素修正時の条件修正が少ない(case追加とか)静的ポリモーフィズムっぽい設計にできる(条件分岐があっていいならジェネリックで代替可)
void M_1_1() { printf("1_1"); };
void M_1_2() { printf("1_2"); };
void M_1_3() { printf("1_3"); };
void M_2_1() { printf("2_1"); };
void M_2_2() { printf("2_2"); };
void M_2_3() { printf("2_3"); };
int main() {
// 擬似二次元配列
method_ptr_t questions[2 * 3]; // 関数ポインタで型を指定。配列なので明確に型指定が必要で auto が使えない(逆に autoを使うラムダやファンクタはこれが不可能)
questions[0 * 3 + 0] = M_1_1; // [0,0]
questions[0 * 3 + 1] = M_1_2; // [0,1]
questions[0 * 3 + 2] = M_1_3; // [0,2]
questions[1 * 3 + 0] = M_2_1; // [1,0]
questions[1 * 3 + 1] = M_2_2; // [1,1]
questions[1 * 3 + 2] = M_2_3; // [1,2]
int sectionNum;
int methodNum;
printf("1から2までで入力して");
scanf_s("%d", §ionNum);
printf("1から3までで入力して");
scanf_s("%d", &methodNum);
int questionNum = (sectionNum - 1) * 3 + (methodNum - 1);
questions[questionNum](); // 配列から呼び出して、選択された関数を実行
}
// ラムダ式
// クラスの()演算子をオーバーロードして「インスタンス名()」でオブジェクトでありながらそのまま関数を呼び出せるようにしたファンクタという関数オブジェクトを、クラス名とか省きまくって簡易的に表現したもの。
struct Functor { // ファンクタ
int operator()(int v) { return v * 2; } // 関数呼び出し演算子() を定義。ラムダ式はこの部分だけを切り取って他を省いている。
};
int main() {
Functor fn = Functor();
fn(3); // インスタンス() で関数呼び出しが可能。オブジェクトが関数を持つ。
}
// ラムダは関数ポインタと異なり右辺値なので、変数に格納せずそのまま使うことも auto で型推論を使えば変数に保持もできる。
int main() {
//「 [キャプチャ...] (引数...) mutable 例外仕様 属性 -> 戻り値の型 { 関数処理 } 」の形で、不必要な物は省略できる。最小の形は何もしないラムダの []{} 。
[](int n) { std::cout << "lambda " << n << std::endl; }(100); // 関数オブジェクトなので()で実行できる。関数オブジェクト名(引数...);
// auto で型推定を使った時だけ変数に代入できる。
auto printX1 = [](int x) { std::cout << x * 1 << std::endl; };
auto printX2 = [](int x) { std::cout << x * 2 << std::endl; };
printX1(1);
printX2(1);
auto printXs[2] = { printX1 ,printX2 }; // 明示的な型指定は不可能なので配列にはできない(関数ポインタは可能、std::function使えば遅くなるけど可能になる)
// ラムダ式はクラスから派生しているので、メンバ変数(キャプチャ)を持てる。引数では仮引数としてコピーや参照で初期化され、キャプチャではメンバ変数としてコピーや参照で初期化される
// キャプチャはメンバ変数を定義するのと同じため、呼び出されたタイミングではなく、ラムダが定義されたときに値が初期化される(引数は呼び出したときの値が使われる)
int x = 0;
int y = 10;
auto allCopy = [=]() { std::cout << x << ", " << y << std::endl; }; // キャプチャ時にはコピーするか参照するかを変数毎に設定できる。全てをコピーキャプチャ
auto copyAndRef = [x, &y]() { std::cout << x << ", " << y << std::endl; }; // xをコピー、yを参照キャプチャ
auto allRef = [&]() { std::cout << x << ", " << y << std::endl; }; // 全てを参照キャプチャ
auto refAndCopy = [&x, =]() { std::cout << x << ", " << y << std::endl; }; // xを参照、それ以外をコピーキャプチャ
x = 1;
y = 11;
allCopy(); // 0, 10 呼び出したときの値ではなく、ラムダ定義時初期化された値が入っている
copyAndRef(); // 0, 11
allRef(); // 1, 11
// また、キャプチャした変数を変更できるのは参照キャプチャのみで、コピーキャプチャは mutableを付けないと変更できない
[&x]() { x += 1; }();
[y]() { y += 1; }(); // エラー。コピーキャプチャした変数は変更不可
[y]() mutable { y += 1; }(); // mutableにするとコピーキャプチャした変数も変更可能(コピーなので元の変数は不変)
allRef(); // 2, 11
}
// ラムダ式では、ラムダ式外の上位スコープにある変数は引数として渡すかキャプチャしないと使用できない(条件分岐やブロック文みたく上位スコープにある変数を無断で使用するみたいなのはできない)
// 引数ではなくキャプチャを使わなければ表現できない状況はないが、上位スコープにある変数を沢山使う時には引数を逐一用意する必要がないのでキャプチャを使うと便利
int main() {
// 上位スコープにある変数を沢山使うとき
int a = 1; int b = 2; int c = 3; int d = 4; int e = 5; int f = 6; int g = 7; int h = 8; int i = 9; int j = 10;
auto capture = [=]() { printf("%d %d %d %d %d %d %d %d %d %d", a, b, c, d, e, f, g, h, i, j); }; // キャプチャにすればラムダ式の上位スコープにある変数をそのまま使える
capture(); // 呼び出し側も短い
auto augment = [](int a, int b, int c, int d, int e, int f, int g, int h, int i, int j) { printf("%d %d %d %d %d %d %d %d %d %d", a, b, c, d, e, f, g, h, i, j); }; // 引数にすると長い
augment(a, b, c, d, e, f, g, h, i, j); // 呼び出し側も長い
}
// またコピーキャプチャした値は、コピーした時点で決定しそのラムダが消えるまでコピーは残るため、ラムダを返す関数でキャプチャを利用すれば、ラムダ内のパラメータを予め指定でき、引数で指定する部分を全く無くしたり少なくできる(つまり、機能の作成部分と利用部分をより分担できる)。
auto Maltiply_capture(string str, int a) { // 特定の数を掛けるラムダを返す
return [=](int b) mutable -> void { // ラムダを返すが。str と a はキャプチャしていて return 時に既に式に組み込まれるので、ラムダ使用時には 引数のbのみを指定させればいい。
std::cout << str << a * b << std::endl;
};
}
auto Maltiply_augment() { // 特定の数を掛けるラムダを返す
return [](string str, int a, int b) mutable -> void { // ラムダを返すが、何もキャプチャしていないので、ラムダ使用時には str も a も b も指定する必要がある。
std::cout << str << a * b << std::endl;
};
}
int main() {
auto maltiply10 = Maltiply_capture(string("Multiple10:"), 10); // ラムダ作成時にはオプションが多い
auto maltiply100 = Maltiply_capture(string("Multiple100:"), 100); // ラムダ作成時にはオプションが多い
maltiply10(5); // ラムダ使用時にはオプションが少なくて済む
maltiply100(5); // ラムダ使用時にはオプションが少なくて済む
auto maltiply10_a = Maltiply_augment();
auto maltiply100_a = Maltiply_augment();
maltiply10_a(string("Multiple10:"), 10, 5); // ラムダ使用時のオプションが多い
maltiply100_a(string("Multiple10:"), 100, 5); // ラムダ使用時のオプションが多い
}
// ただし注意すべきは、ラムダ式は返り値として返されたときや他のスレッドからアクセスされた時に元々の関数より上位のスコープでも生きるようになるが、キャプチャした変数はメンバ変数としてコピーキャプチャではコピーを保持し参照キャプチャでは参照を保持するため、参照キャプチャしているキャプチャ先の変数が先に破棄されてしまう可能性がある。
// そのため、返り値として返したり、他のスレッドからアクセスされるようなラムダ式は、パフォーマンスが落ちても参照キャプチャではなく、コピーキャプチャを利用する必要がある。
// なんと引数を auto にするだけなんとジェネリックラムダも可能。
int main(){
auto fn = [](auto a, auto b) {
std::cout << a << " " << b << std::endl;
};
fn("hello", "world");
fn("string", 10);
fn(100, 1.23f);
}
// C++14からはキャプチャに任意の式を書くことができ、キャプチャ変数の名前を変更したり、関数を呼んだ結果をキャプチャしたり、ムーブキャプチャできる
int main() {
// 簡単なムーブキャプチャ使ったクロージャ
auto sequence = [](int n) {
auto y = std::make_unique<int>(n);
//return [x = y]() mutable {} // unique_ptrはコピー出来ないのでコンパイルエラー
return [x = std::move(y)]() mutable { // moveは可能。moveするためには初期化キャプチャが必要。ムーブキャプチャは上手く使うとめっちゃ軽量化できる。
std::cout << "hoge " << *x << std::endl;
(*x)++;
};
};
auto s = sequence(0);
auto s2 = sequence(100);
s();
s();
s2();
s2();
}
// 変数として状態を保持できるクロージャという使い方も出来るが、個人的に正直そこまでいけばクラス作れば良いし読みにくいだけに感じるので省略する。あと、引数のキャプチャとかメンバ変数のキャプチャとかもあるけど、気になったら調べて。なんか特に超有用な使い所があるとも思えん。
// std::function
// 関数、ラムダ式、関数オブジェクト、クラスのメンバ関数といった複数の種類の関数や関数オブジェクトを格納できる変数。ラムダ式も配列にできそう。
// 関数ポインタでよりもラムダ式で変数キャプチャができて便利だが、パフォーマンス的には関数ポインタより最大2,3倍遅い(https://qiita.com/suzukiplan/items/e459bf47f6c659acc74d)使わんで良い?
// 関数オブジェクトまとめ。
// キャプチャを利用できる点でラムダ式が利便性に超優れるが、配列等の形にするなら auto が使えないため配列にしたい時は std::function(遅い) か関数ポインタ(早い)を使う必要が出てくる。
// ラムダ式使うときはキャプチャを上手く使うとさらに超便利になるが、上位スコープを抜けても寿命が生き続けるような場合は参照キャプチャしないように注意する(それ以外は最適化のため参照使う)
///ーー 列挙型 ーー///
// 複数の状態 or 選択肢によって条件分岐をしたい時、特に選択肢を用意する時にはほぼ必ず用いる。
// 可読性もすぐれているし、整数値として連番を振り分けられるため、要素数なども一緒に記録できる。
enum Question // 列挙型は内部で読み取り専門の整数として扱われる。デフォでは 0 から順に値が割り振られる。
{
Q_a, // 0
Q_b, // 1
Q_c, // 2
Q_d, // 3
AllQuestionNum // 4 ← 全状態(選択肢)の数を得たいなら、最後に用意する。
};
enum Question2
{
Q_A, // 0
Q_C, // 1 // Q_b が削除
Q_D, // 2
AllQuestionNum // 3 ← 仮にどこかの列挙子が増減しても自動で対応される。
};
enum Answer
{
A_Q_a, // 0
A_Q_b, // 1
AllQuestionNum, // 2
DeclineAnswer = 10, // 10 // 列挙子 = 整数値 で値を指定できる
SkipAnswer // 11 // 値が指定された列挙子以降は順に割り振られる
};
Question day = Q_a; // 使用例
int x = day; // 数値型に enum の値を代入することも可能。逆は不可。
// enum で宣言した列挙型だと、列挙子は名前は被らないようにする必要がある。
enum ToDay
{
Happy,
Unhappy
};
enum Mental
{
Happy, // Sun は enum Day 内で定義済みのためエラー
Sad
};
// C++ の機能で、enum class で列挙型を定義すると他の列挙型の列挙子と名前が被っても大丈夫。重複考えるとこっち使うべき
enum class ToDay
{
Happy,
Unhappy
};
enum class Mental
{
Happy, // ToDay::Happy と Mental::Happy は区別されるためOK
Sad
};
// ただし、enum class を扱うには 型名::列挙子名 とする。
ToDay day = ToDay::Happy; // OK
ToDay day = Happy; // エラー
// 同じく数値型に代入できるが、こっちは明示的な型変換が必要。
int x = ToDay::Happy; // エラー
int x = static_cast<int>(ToDay::Happy);
///ーー 構造体 ーー///
// 複数の型の異なる変数を格納するもの。C言語では関数が中に入れないので、配列の「型に依存しない」版と同じ。データを扱うときにもよく使う。
// C++ だとクラスとほぼ一緒だが、構造体はデフォルトアクセスが public、クラスは privateなので、外にメンバをほぼ全て外に公開していい時に構造体を使うことが多い。
// 「 struct 構造体名 { 変数の宣言のみ... } 」
struct student {
int id;
char name[256];
int age;
};
int main() { // 使い方は、内部のメンバへのアクセス以外、他の型と同じ。
student data1; // インスタンス化。他の型と同じで宣言するだけで初期化しなくても自動で初期値が不定値で入る。
data1.id = 1; // アクセスは「 構造体名.変数名 」
strcpy(data1.name, "山田太郎");
data1.age = 18;
printf("学生番号:%d 名前:%s 年齢:%d¥n", data1.id, data1.name, data1.age);
// 配列にもできる
student data2[] = {
{ 1,"山田太郎",18 },
{ 2,"佐藤良子",19 },
{ 3,"太田隆",18 },
{ 4,"中田優子",18 }
};
for (int i = 0; i < 4; i++) {
printf("学生番号:%d 名前:%s 年齢:%d¥n", data2[i].id, data2[i].name, data2[i].age);
}
// 構造体のポインタ
student* data3;
data3 = &data1; // 構造体のアドレスはintとかの普通の変数と同じく &構造体名 で代入する。
// 構造体のポインタから参照値へのアクセスは「構造体名->変数名」
printf("学生番号:%d 名前:%s 年齢:%d\n", data3->id, data3->name, data3->age);
}
///ーー 共用体 ーー///
// 複数の可能性ある型の中から一つの型を使うときに用いられる。
union abc { // char か int か double の「いずれか一つ」になる
char a;
int b;
double c;
};
// 使い所はメモリの節約。例えば、複数のデータ型を取る可能性のデータを1つの配列にまとめたい時、共用体を用いると管理が楽になる上にメモリが節約できる。同じことを構造体でやろうとすると、使っていないメンバーの分のメモリが浪費される。
// 正直クラスが使えるC++では不要かも。継承使って親クラスのポインタでインスタンス作れば、使いたい子クラス代入するだけで複数の可能性ある子クラスの中から1つの型だけ使えるし、、。(メモリを動的に確保するか否かの違いはあるか、クラス使うと動的確保絶対で、共用体使ったら静的確保でスタックも使える)
///ーー クラス ーー///
//---------------car.hpp
//#ifndef CAR_H
//#define CAR_H
class Car {
// クラスはメンバに対するデフォルトのアクセスが private、構造体は public。
// クラス内のメンバはアクセス修飾子がつく。原則メンバ関数に public、メンバ変数に private/protected を付けて外部からの内部メンバ変数へのアクセスを制限して隠蔽する(カプセル化する)。
protected:
// private や protected などの隠蔽されたメンバ変数には、それとわかるように m_ を頭文字に付ける
int m_fuel; // 燃料
int m_distance; // 移動距離
public:
void Move();
void Supply(int fuel);
// カプセル化されたメンバ変数に対してアクセスする public なメンバ関数をアクセッサーといい、C++ではメンバ変数の数だけ Set/Get のアクセッサーが生じる。
// アクセッサー関数の名前にはわかりやすく set_ / get_ の頭文字を付けることが多いが、メンバ変数をより隠蔽してセキュリティを高める場合は、メンバ変数名が推測できないような名前にする。
// また、非メンバ関数なのにメンバ内に関数を書いてアクセッサー不要でメンバ変数に直接アクセスできるフレンド関数というものもある(アクセッサー不要になるメリットはあるけど、複数の箇所からメンバ変数にアクセスする場合にはアクセッサーを介してアクセスを一括で管理できないデメリットが生じる。まぁ使いそうになったら調べて)。
void set_fuel(int fuel) { m_fuel = fuel; }; // get/set のみは大体短くなるので、インライン展開を狙ってクラス内に定義までまとめて記す。
void set_distance(int distance) { m_distance = distance; };
// メンバ関数の {} の前に constをつけると、その関数内で自インスタンスのメンバ変数が変化しないことを保証する。
// また constのインスタンスはメンバ関数にアクセスできないが constメンバ関数にだけはアクセスできるため、constのインスタンスが内部メンバ変数にアクセスする時は constメンバ関数を定義する
int get_fuel() const { return m_fuel; };
int get_distance() const { return m_distance; };
// 同名同引数の 非constメンバ関数と constメンバ関数がある時は、オブジェクトが 非constの時は前者、constの時は後者を使うように勝手に使い分けされるようになる。
int constMemberFunc() {}; // 非constオブジェクトが constMemberFunc() を呼ぶときに使われる
int constMemberFunc() const {}; // constオブジェクトが constMemberFunc() を呼ぶときに使われる
private:
// クラスの内部にクラスを設けることも可能。アクセスをこのCarクラス関連にしたい時とか、このCarクラス用のエラー判別クラスを使ったりするときに使える。
// ただし、これするとヘッダとソースに分けて実装する時とかに頭にクラス名だけ名前増えるのでやりすぎないように。 Car::InnerClass::InnerClass2.InnerClass2Function(){}; みたいな
class InnerClass {/* ~なんかクラスのやつ、省略~ */ };
};
//#endif
//---------------car.cpp
// #include "car.hpp"
void Car::Move() {
if (m_fuel >= 0) { // なんと同じクラス内なら、メンバ関数内ではメンバ変数には修飾子なしでアクセスできる。
m_distance++;
m_fuel--;
}
cout << "移動距離:" << m_distance << endl;
cout << "燃料" << m_fuel << endl;
}
void Car::Supply(int fuel) {
if (fuel > 0) {
m_fuel += fuel;
}
cout << "燃料" << m_fuel << endl;
}
//---------------main.cpp
// #include "car.hpp"
int main() { // 使い方は、内部のメンバへのアクセス以外、他の型と同じ。内部メンバのアクセスは構造体と同じ。
Car car; // インスタンス化。他の型と同じで初期化しないと Car型の不定値が入る。
int num = 100;
car.set_fuel(num);
cout << car.get_fuel() << endl;
Car* c = new (nothrow) Car(); // new で動的にメモリ確保してインスタンス生成。他の型と同じ
assert(c != nullptr);
c->set_fuel(num); // 構造体と同じで、インスタンスの値のアクセスにはインスタンスにドット(.)、インスタンスのポインタにアロー(->)を使う。
delete c;
c = nullptr;
Car cars[5]; // 他の型と同じで、配列にもできる
}
// ちなみに、C++ だと、this は、そのメンバ関数を内包するインスタンスへの参照値ではなく、メンバ関数を内包するインスタンスへの「ポインタ」になる。
// this ポインタは、全ての staticでないメンバ関数に暗黙的に引数として渡され、全ての staticでない関数内のローカル変数として使用できる。
// また、クラス(構造体)型のサイズはメンバ変数と virtual関数の有無とアライメントによって決定し、メンバ関数の数や種類には依存しない。
// 例えばこのCar クラスだと int m_fuel;と int m_distance;で int2個分なので8バイトの大きさのクラス型になる(「アライメントとパディング」にて後述)。
///ーー static メンバ変数・メンバ関数 ーー///
// クラス固有でインスタンス(オブジェクト)を必要としないし、同クラスでは共有でインスタンスごとに変化しない。多分 C# の static と似た感じ。
// staticメンバ変数は複数のクラスに継承して継承先から操作しても、継承元の static メンバ変数は単一のみで、全ての継承先でも値は共通になる。
// 静的領域に確保されるので、グロ変と同じでクラス等の大きな変数を取るべきではない、扱うならポインタにして扱う。また、グロ変と同じでヘッダ内では宣言しかできず、定義はどこかのソースで行う。
// staticメンバ変数を使うタイミングとしては、クラスで共通して使う値があってインスタンス生成ごとにメモリを消費したくない時とか、コンストラクタとデストラクタで±1ずつとかしてインスタンスの現状数を数えるときとか、シングルトン作る時とか。
// staticメンバ関数は、内部で staticメンバ変数しか使えない(インスタンスが無くても使えるのが staticメンバ変数しかないから)。同じ理由で、内部でthis ポインタも使えない。
// また、staticメンバ変数の初期化タイミングの観点から、staticメンバ関数の定義と staticメンバ変数の定義は同じ場所でする必要がある(基本実装は同じcppにまとめるからあまり意識しなくてもOK)
// staticメンバ関数を使うタイミングとしては、自分のインスタンスが存在しない状況からオブジェクトを生成する時くらい?(シングルトンとかでGet時になければ作るとか)
// また、static メンバ変数は静的領域に確保され、クラスのサイズには含まれない
class StaticMemberExa { // このクラスのサイズは非 staticの int n2; の4バイト分。
static int n1; // こいつは静的領域に確保されるので、プログラム起動中に常にメモリに常在する。
int n2;
};
// ただし、グロ変数/関数や staticメンバはテストを行う際に非常に妨げになり、機能を実装した際にはテストは必要不可欠なため、これらの使用は避けるべきとされている。
///ーー mutable(キャッシュやログのみ) ーー///
// mutable指定子は、クラスのメンバ変数に対して適用されるキーワードで、使うと constメンバ関数内でもメンバ変数を変更できる。
// これによってオブジェクトの不変性を担保しながら、一部のメンバ変数をキャッシュやログなどの補助的な情報として扱えるようになる。
// 他のメンバ変数に多用すると constメンバ関数が無意味になるので、使用は補助的な部分に限定すべき。
//---------------
struct Logger { // カウンターを内蔵するログ出力クラス
mutable std::mutex mutex; // 複数のスレッドからアクセスする情報を担うことが多いので、大抵競合や不整合を防ぐ同期処理をする。std::mutexには include <mutex>がいる。
mutable int logCount;
Logger() : logCount(0) {}
void log(const std::string& message) const {
std::lock_guard<std::mutex> lock(mutex); // このスコープ内だけスレッドセーフ(非同期処理は後述)
std::cout << message << std::endl;
logCount++;
}
void clearLog() const {
std::lock_guard<std::mutex> lock(mutex); // このスコープ内だけスレッドセーフ(非同期処理は後述)
printf("Log count is cleared.");
logCount = 0;
}
};
int main() {
const Logger logger;
logger.log("Hello, World!");
printf("%d\n", logger.logCount);
logger.clearLog();
}
//---------------
struct Calculator { // データキャッシュを使った計算クラス
mutable std::map<int, int> cache;
int calculate(int value) const {
auto it = cache.find(value);
if (it != cache.end()) {
return it->second; // キャッシュから結果を返す
}
else {
int result = value * value; // 計算
cache[value] = result; // 結果をキャッシュする
return result;
}
}
};
int main() {
Calculator calc;
std::cout << calc.calculate(4) << std::endl;
std::cout << calc.calculate(4) << std::endl; // キャッシュされた値を使用
}
//---------------
// Observerパターンを mutableで実現(これ補助的な機能やないけどいいんか?mutable 使わずに普通に実装したほうが良さそうやけど、、普通に Observerパターンまとめよう)
struct IObserver { // Observerの基底インタ
virtual void update() = 0;
};
struct Observer1 : public IObserver { // Observerクラス
void update() override {
std::cout << "Observer1 updated." << std::endl;
}
};
struct Observer2 : public IObserver { // Observerクラス
void update() override {
std::cout << "Observer2 updated." << std::endl;
}
};
struct Subject { // Observableクラス
mutable std::vector<IObserver*> observers; // 全Observer 管理リスト
void addObserver(IObserver* observer) const { // Oberver登録
observers.push_back(observer);
}
void notifyObservers() const { // 全Observer に通知
for (IObserver* observer : observers) {
observer->update();
}
}
};
int main() {
Subject subject;
Observer1 observer1;
Observer2 observer2;
subject.addObserver(&observer1);
subject.addObserver(&observer2);
subject.notifyObservers();
}
// Observerパターンまとめて、上のが普通に実装で競うなら悪例として消す。
///ーー クラス(構造体)の継承 ーー///
// 継承はクラス(構造体)の定義時に、クラス名の後ろに「:public 親型名」で可能。
// C++ では多重継承が可能だが、設計的な危険性が無限に生じるのでインターフェース等の特殊な型の継承以外、多重継承は用いてはならない(実質C#と同じノリ)。また、C#みたいに、そもそも継承を入れ子的に使いすぎない方がいいとかあるんかな?結果的に固定的に組み込まれちゃうから、追加修正時に致命的な編集不可点とか、他の継承関係にあるクラス全般直さないといけなくなるとかで、、。しらべなおそう。https://qiita.com/sakastudio_/items/6fd1f9b1b30b656b273e#%E7%B6%99%E6%89%BF%E3%81%AF%E3%81%97%E3%81%AA%E3%81%84
// 子クラスは、親クラスの public、protected メンバにアクセスでき、private メンバにはアクセスできない。
// また、後述の「型変換・キャスト」で詳細に触れるが、継承機能のため、子クラスを指定された関数の引数に中身が親クラスのインスタンスを代入して暗黙的にダウンキャストしようとしてバグったり(コンパイル時ではエラーにならない)、暗黙的なアップキャストを生じる可能性があるため、クラス(構造体)を扱うときには、インスタンスが格納されている変数の型(入れ物)と、その中身のインスタンスの型を常に意識する必要がある。(ここでは、その型にキャストしてもバグらない最大深度の派生クラス(整地されたメモリ範囲)のことをインスタンスの型と呼んでいる)
// ambulance.hpp
//#ifndef AMBULANCE_H
//#define AMBULANCE_H
//#include "car.hpp"
class Ambulance : public Car
{
public:
Ambulance();
~Ambulance();
public:
void Move(); // オーバーライド。C# みたいに修飾子がなく、ただ親クラスと同じように宣言して定義するだけでも生じる。
void sevePeople(); // 救急救命活動
private:
int m_number;
};
//#endif
// ambulance.cpp
//#include "ambulance.hpp"
//#include <iostream>
Ambulance::Ambulance() : m_number(119)
{
cout << "Ambulanceオブジェクト生成" << endl;
}
Ambulance::~Ambulance()
{
cout << "Ambulanceオブジェクト破棄" << endl;
}
void Ambulance::Move() // オーバーライドのやつ。
{
if (m_fuel >= 0) {
m_distance += 1; // 距離移動
m_fuel -= 2; // 燃料消費
}
cout << "移動距離:" << m_distance << endl;
cout << "燃料" << m_fuel << endl;
}
void Ambulance::sevePeople()
{
cout << "救急救命活動" << endl << "呼び出しは" << m_number << "番" << endl;
}
// main.cpp
//#include "car.hpp"
//#include "ambulance.hpp"
int main() {
Car c;
c.Supply(10);
c.Move();
Ambulance a;
a.Supply(10);
a.Move();
a.sevePeople();
return 0;
}
///ーー オーバーライドと virtual / final とインタフェ ーー///
// 上の継承でも言及したが、C++ では子クラス側で親クラスと同名の関数を定義することで、自動的にオーバライドが実現される。
class Animal { public: void say() { puts("・・・"); } };
class Cat : public Animal { public: void say() { puts("にゃー"); } }; // 親クラスと同名の関数を定義
class Dog : public Animal { public: void say() { puts("ワン!"); } }; // 〃
int main() {
Animal animal;
Cat cat;
Dog dog;
animal.say(); // "・・・"
cat.say(); // "にゃー"
dog.say(); // "ワン!"
}
// ただし、インスタンスから呼ばれるメンバ関数は「インスタンスの中身の型」ではなく、「インスタンスを格納している変数の型」のメンバ関数を呼び出す。
// そのため、オーバーライドすると、親クラスの型にアップキャストされたインスタンス(インスタンスの中身は子クラスだが、インスタンスを格納している変数の型は親)では、親クラスで定義された関数が呼び出される。
// つまり、先に宣言した包括的な親クラスの型をした変数に、後から処理に応じてより内包的な子クラスのインスタンスを当てはめるても、子クラスにダウンキャストしないと子クラスの機能が使えない。
int main() {
Animal thisAnimal; // 先により包括的な親の型の変数を宣言
// ~~ thisAnimal に何入れるか決めるための色々な処理 ~~ //
thisAnimal = Dog(); // 後からより具体的な子クラスのインスタンスを指定。
thisAnimal.say(); // "・・・" インスタンスの中身の型は Dogだが、インスタンスを格納している変数の型は Animalなので、子クラス Dogの say()は呼ばれず Animalの say()が呼ばれる
((Dog)thisAnimal).say(); // C++はダウンキャストはポインタか参照変数にしか行えないのでエラー
Dog d;
Animal& rThisAnimal = d;
((Dog&)rThisAnimal).say(); // "ワン!" 参照変数にしてダウンキャストしたらいける
// ポインタも同じ
Animal* pThisAnimal = nullptr;
// ~~ thisAnimal に何入れるか決めるための色々な処理 ~~ //
pThisAnimal = new (nothrow) Dog();
assert(pThisAnimal != nullptr);
pThisAnimal->say(); // "・・・"
((Dog*)pThisAnimal)->say(); // "ワン!" 子クラスのポインタにダウンキャストすれば出来る。けど結局ダウンキャスト危ないしこんなの逐一やってられない
}
// 親クラスの関数に virtual をつけると、「ポインタ変数と参照変数の場合に限り、最後に代入した時の右辺のキャスト前の型」の関数を実行するようになる。(結局どのような形をとっても、中身の型が何かということはダウンキャストしないと関係しない)
class Animal2 {
public: virtual void say() { puts("・・・"); }
public: virtual void sayYes() final { puts("Yes"); } // final をつけると以降継承による派生先でのオーバーライドを禁止できる。仮想関数は処理が重く、final を付けて非仮想関数化すれば constみたいに処理を軽くできる。また、親クラスで一度でも virtual指定されたメンバ関数は、派生クラスで virtualを書かなくても自動的に全て virtualになるため、以降派生先でオーバーライドをしない場合は final を毎回付けるべき。
};
class Cat2 : public Animal2 { void say() override { puts("にゃー"); } }; // override をつけると親の関数に virtual の強要が行える & わかりやすいのでこれも使用推奨
class Dog2 : public Animal2 { void say() override final { puts("ワン!"); } }; // override と final は共存可能。
int main() {
Animal2 animal1 = Animal2(); // 入れ物は親の型、中身のインスタンスも親の型
animal1.say(); // "・・・"
Animal2 thisAnimal1 = Dog2(); // 入れ物は親の型、中身のインスタンスは子の型
thisAnimal1.say(); // "・・・" 中身(実体)は無関係 → ポインタでも参照変数でもないので入れ物の型が呼ばれる → 親が呼ばれる
Animal2* pAnimal1 = new (nothrow) Animal2; // 親のポインタに、親のポインタを代入。
assert(pAnimal1 != nullptr);
pAnimal1->say(); // "・・・" 中身(実体)は無関係 → ポインタなので最後に代入した右辺の型が呼ばれる → 親が呼ばれる
Animal2* pAnimal2 = new (nothrow) Dog2; // 親のポインタに、このポインタを代入。
assert(pAnimal2 != nullptr);
pAnimal2->say(); // "ワン!" 中身(実体)は無関係 → ポインタなので最後に代入した右辺の型が呼ばれる → 子が呼ばれる
// 中身の型が関係ない例。
Dog2 d;
Animal2 thisAnimal2 = d; // 入れ物は親の型、中身のインスタンスは子の型
Animal2* pAnimal3 = &thisAnimal2; // 最後に代入した右辺の型は親。
pAnimal3->say(); // "・・・" 中身(実体)のインスタンスは子でも、ポインタで最後に代入した右辺の型が親なので、親の型のメンバ関数が呼ばれてる。
Animal2& pAnimal4 = d; // 入れ物は親の型、中身のインスタンスは子の型。
pAnimal4.say(); // "ワン!" 参照変数で最後に代入した右辺の型が子なので、子のメンバ関数が呼ばれてる。
Animal2* pAnimal5 = static_cast<Animal2*>(&d); // ちなみにアップキャスト前の型のことなので、このように明示的にアップキャストを挟んでも、基準は子になる
pAnimal5->say(); // "ワン!" 子のメンバ関数が呼ばれてる
// この特徴を使えば、親のポインタ/参照変数を先に宣言して、そこにどの子の型(或いは親の型)のポインタ/参照変数を代入するかで、機能を切り替えられる。
}
// 親側では全く機能を実装せず、子クラス毎に機能を実装させるつもりなら、空の実装(純粋仮想関数)を持つ抽象クラスにして、継承先で実装を強制させる。
// 一つでも純粋仮想関数を持つ親クラスは、メンバ関数が欠けているので自身の型のインスタンスは生成できず、関数の補完がされる継承先の子のインスタンスしか生成できない。
class Animal3 { public: virtual void say() = 0; // 「virtual 返り値型 関数名 = 0;」で純粋仮想関数。
public: virtual void sayYes() final { puts("Yes"); } };
class Dog3 : public Animal3 { void say() override final { puts("ワン!"); } };
int main() {
Animal3 animal3; // これはエラー。自身のインスタンスは生成不可。
Dog3 dog3; // これはOK。親で欠けてたメンバ関数は補完されてるから。
Animal3* thisAnimal = new (nothrow) Dog3;
assert(thisAnimal != nullptr);
thisAnimal->say(); // "ワン!"
thisAnimal->sayYes(); // "Yes"
}
// インターフェースは全て純粋仮想関数でできたクラス。基本的には似た種類の子クラスたちがこれを共通に継承することで、複数のクラスに共通の機能を実装強制させられる設計ができる。
// そして、インターフェース型のポインタを用いて子クラスのポインタを代入することで、動的な(実行時に振る舞いが決まる)ポリモーフィズム(同名で同形式なのに異なる機能を持たせれること)を実現できるのがこの設計の主な特徴。
// また、インターフェースは多重継承が許可されており、多重継承することで複数の機能を共通して持たせることができる。
class Animal4 { public: virtual void say() = 0;
public: virtual void sayYes() = 0; }; // Animal 機能
class LivingThing { public: virtual void takeBreath() = 0; }; // LivingThing 機能
class Cat4 : public Animal4, public LivingThing { // インターフェースを多重継承。Animal4と LivingThingの機能を Cat4型用にカスタマイズして搭載。
public:
void say() override final { puts("にゃー"); }
void sayYes() override final { puts("Yesにゃー"); }
void takeBreath() override final { puts("すぅー、はぁー"); }
};
class Dog4 : public Animal4, public LivingThing { // インターフェースを多重継承。Animal4と LivingThingの機能を Dog4型用にカスタマイズして搭載。
public:
void say() override final { puts("ワン!"); }
void sayYes() override final { puts("Yesワン!"); }
void takeBreath() override final { puts("ハッ、ハッ"); }
};
// 動的ポリモーフィズム。親のポインタ/参照変数を先に宣言して、そこにどの子の型(或いは親の型)のポインタ/参照変数を代入するかで、全く同じ書き方で異なった処理を行える。
int main() {
Animal4* animal4s[3]; // Animal 機能を持った子クラスから、共通の関数 say() で各子クラス固有の Animal機能を使う。
animal4s[0] = new Cat4();
animal4s[1] = new Dog4();
animal4s[2] = new Cat4();
for (int i = 0; i < 3; i++) {
animal4s[i]->say(); // 一つの書き方で、代入時の型によって処理が勝手に変わっていく。
}
LivingThing* livingThing[3]; // 多重継承していて LivingThing 機能も持ってるので、同じ子クラスたちを使って、この機能によるポリモーフィズムもできる。
livingThing[0] = new Cat4();
livingThing[1] = new Dog4();
livingThing[2] = new Cat4();
for (int i = 0; i < 3; i++) {
livingThing[i]->takeBreath(); // 一つの書き方で、ポインタ代入時の型によって処理が勝手に変わっていく。
}
}
// またインターフェースを使うと、関数オブジェクトみたいな形で機能を使う部分と機能を実装する部分を分けられるため、上位の部品が下位の部品を使いたくなった時の依存関係の逆転を解決できる。
struct UpperClass {
LivingThing* livingThing = nullptr;
void UpperFunction() { // 上位クラス特有の処理
/* ~なんかいろいろ処理~ */
livingThing->takeBreath();
/* ~なんかいろいろ処理~ */
}
};
struct DownerClass {
int flag;
UpperClass upper;
DownerClass(int n) : flag(n) {}
void DownerFunction() {
if (flag == 0) {
upper.livingThing = new Cat4(); // 下位クラス側で上位クラスの処理を決定できる。本来は上位クラス内で下位クラスを取得して、その中からフラグを取得する必要があった(依存関係の逆転)
upper.UpperFunction();
}
if (flag == 1) {
upper.livingThing = new Dog4(); // 下位クラス側で上位クラスの処理を決定できる。
upper.UpperFunction();
}
}
};
// また、関数の引数としてインスタンスを渡すときにも、継承関係は意識する必要があるので注意。
struct Parent {
virtual void print_name() { puts("Parent"); }
};
struct Child : public Parent {
virtual void print_name() { puts("Child"); }
};
void function1(Parent data) {
data.print_name();
}
void function2(Parent* data) { // ポインタ渡しや参照渡しだと、普通にポインタや参照変数を使って動的ポリモーフィズムするのと同じ挙動になる。
data->print_name();
}
void function3(Parent& data) { // ポインタ渡しや参照渡しだと、普通にポインタや参照変数を使って動的ポリモーフィズムするのと同じ挙動になる。
data.print_name();
}
int main() {
Child child;
function1(child); // "Parent"
function2(&child);// "Child" 引数がポインタ/参照変数で、内部で仮引数に代入されるこれら引数は子の型なので、子の関数が呼ばれる
function3(child); // "Child"
}
///ーー 型変換・キャスト ーー///
int main()
{
// キャストは暗黙的に行われることも多い。
int impli = 12.34f; // 互換可能な型のキャスト
Animal animal = new Cat(); // アップキャスト
Cat cat = new Animal(); // ダウンキャストは暗黙的キャストがコンパイラから許されない(危険だから)。
// ただ、子の型が指定された関数の引数に、親の型のインスタンスが代入されてダウンキャストが生じる形になった場合は、コンパイル時では見つけられずエラーにならないので注意。
// void Function (Cat cat){} の引数に Animal animal が代入されちゃうときとか。
// 暗黙キャストよりも、明示的にキャストした方がコードの意図が分かりやすいし見逃しも減る。
// (キャスト先の型) を指定すれば、明示的にキャストができる。
double x = 101023.98;
int y = (double)x; // 浮動小数点数型からint型などの整数へキャストすると、範囲外(小数点以下)が切り捨てられる
char z = (int)x; // 型の範囲上限以上の値を代入するとオーバーフローで値がバグるので注意。char型の値の範囲は 0~255 か -128~127。
// ただしこのC言語のやり方でキャストすると安全性の面で色々とまずい。
int* aa = (int*)(0x00001000); // 整数をポインタにしたり
int bb;
double* cc = (double*)&bb; // 全く違う型のポインタにしたり
const int dd = 100;
aa = (int*)ⅆ // constを外したりできちゃう
// また暗黙キャストもオーバーフローに気づきにくい
long long a = 100000000000000LL;
int b = a; // int に long を
void* p = &a; // ポインタに longを
int* c = (int*)p; // longのポインタをintにつこーてる
// ちなみに C++ では、デフォではアップキャストは普通の変数でもポインタ変数でも可能だが、ダウンキャストはポインタ変数(と多分参照変数)でしか行えない(なぜかは謎)。キャスト演算子をオーバーロードしないと行う機能がない。
}
// C++ ではキャストの機能を限定したキャスト演算子が存在する。使うと安全性も増すし、書いた人の意図が伝わりやすくキャストエラー避けれるので使おう。
// static_cast<型名>(値(右辺値でも左辺値でも可))
// ポインタとint間、全く違う型のポインタ間の型変換などの危険を検知してくれる。ただダウンキャストやオーバーフローの危険の検知はない。
// よくある、キャストを行う状況下(アップキャスト、ダウンキャスト、他変換可能な型間の変換)でキャストを明示的に行う時に使う。
// ただし大半のキャストはコレ使うし、結局意図がわからなくなりやすいかもだから、アップキャストとダウンキャストくらいはコメントつけた方がいいかも。
float f1 = static_cast<float>(32);
// const_cast<型名ポインタor型名>(ポインタor参照)
// ポインタや参照のconstを付け外しできる。普通に安全性やばいので constの付け外しが必要になる場合は原則定義側を変えるようにして、普段はこれは使わない。外部ライブラリで constの無い引数をどうしても必要とする困った関数がいたり、非 constメンバ関数の中で constメンバ関数の戻り値の constを外したりするときに使う。
// reinterpret_cast<型名ポインタor型名>(値orポインタor参照)
// 強制キャスト。異なる型のポインター間の変換、生アドレス整数値とポインター型との変換を行うときに使う。ポインタの関係する危険なキャストを強引にやるので普段は使わない。
// dynamic_cast<型名ポインタor型名>(ポインタor参照)
// ダウンキャスト時に、インスタンスの型を基準にして不正なキャストを検知してくれる。ポインタや参照のダウンキャスト(とクロスキャスト)を安全確認しながらに行うときに使う。
// クラスのキャストは、メンバが余分に多くなる分にはバグらない(キャスト後には結局必要なメンバしか使わないから)が、メンバが足りない分には処理がバグる(キャスト後に使おうとするメンバが存在しないから)。そのため、子から親へのアップキャストは子の中に親のメンバが全て入ってるのでメンバが足りてバグらないが、親から子へのダウンキャストは、変数に格納されているインスタンスの型によってはバグる可能性が生じる。
// この危険性から、キャスト失敗を考慮したコードを書く必要があったり、メモリアクセス違反を引き起こす可能性があるので、そもそもなるべくダウンキャストがないコード設計をするべき。ただし、一度ダウンキャストした後にアップキャストする時(クロスキャスト)や、ベースクラスを継承して大量の派生クラスを作ってそれを同じリストに入れてその派生クラスのインスタンス毎に引数などが異なる個別の処理をする時などで、可読静的にもどうしても必要なケースもあるらしい。
class Parent1 {
public:
virtual ~Parent1(){}
};
class Child1 : public Parent1 { private: int member1 = 100; };
class Child2 : public Parent1 { private: float member2 = 1.0f;};
int main() {
Parent1* parent = new Parent1; // 格納された変数は Parent1型、インスタンス(実態)は Parent1型。
Parent1* ch_parent = new Child1; // 格納された変数は Parent1型、インスタンス(実態)は Child1型。これ static_cast使ってもいい
// 以下の3つの状況では、真ん中しか適切にダウンキャストできてないが、コンパイル自体は通るし全てキャスト自体は成功してしまう。
Child1* child1_a = static_cast<Child1*>(parent); // NG. インスタンス(実態)が Parent1型なので、Child1型にダウンキャストすると、メンバ(Child1::member1)が足りなくて処理がバグる。
Child1* child1_b = static_cast<Child1*>(ch_parent); // OK. 格納された変数は Parent1型だけど、インスタンス(実態)が Child1型なので、Child1型にダウンキャストしてもメンバは足りるため、適切に動作する。
Child2* child2 = static_cast<Child2*>(ch_parent); // NG. 格納された変数は Parent1型だけど、インスタンス(実態)が Child1型なので、Child2型にダウンキャストすると、メンバ(Child2::member2)が足りなくて処理がバグる。
// 上の状況を、インスタンスの型を基準にして適切にダウンキャストできるかどうかを検知するようにしたのが、dynamic_cast。
Child1* child1_a = dynamic_cast<Child1*>(ch_parent);
Child1* child1_b = dynamic_cast<Child1*>(ch_parent);
Child2* child2 = dynamic_cast<Child2*>(ch_parent);
// 失敗するとポインタだとnullptrを返し、参照だとstd::bad_castの例外を送出する
if (child1_a == nullptr)printf("child1_a のダウンキャスト失敗");
if (child1_b == nullptr)printf("child1_b のダウンキャスト失敗");
if (child2 == nullptr)printf("child2 のダウンキャスト失敗");
return 0;
}
// ただし、dynamic_castは処理が重いので、明確にダウンキャストが適切に行われているとわかる時は static_castを使った方がいい(上のstatic_cast使ってる例の真ん中みたいに)。てか明確にわかるような設計にして dynamic_castを減らした方がいい。
// また、dynamic_castは仮想関数テーブル使っていて、親クラスが1つ以上 virtualを持った仮想関数がないとできない。
// このキャストの仕組みや、上述した継承関連の仕組みの関係で、クラスを扱う時は、インスタンスが格納されている変数の型(入れ物)と、インスタンスそのものの型(中身)の2つを常に把握しておく必要がある。
// クロスキャスト
// 子が親を多重継承してる時で、変数の型が親Aでインスタンスの型が子の時に、そのまま変数の型を親Bに変えること。ダウンキャストからアップキャストする。
// 実態は子のインスタンスなので、中身的にはアップキャスト先をAからBに変更するだけで、メンバが不足することはないのでOK。
// インタフェのAnimal4を例にすると。
int main() {
// Cat4は Animal4と LivingThingのクラスを継承してる。
Animal4* thisAnimal = new Cat4; // 変数の型が親Aでインスタンスの型が子。①
Cat4* cat = static_cast<Cat4*>(thisAnimal); // ダウンキャスト
LivingThing* thisLivingThing = static_cast<LivingThing*>(cat); // アップキャスト。このダウン→アップの流れがクロスキャスト。②
LivingThing* thisLivingThing = dynamic_cast<LivingThing*>(thisAnimal); // dynamic_castだと一行でクロスキャストかける。②
Cat4* cat = new (nothrow) Cat4; // 初めから変数の型もインスタンスの型も子だと、ダウンキャストを生じなくて済む設計にできるからクロスキャスト生じそうな時はこのやり方で避けた方がいいかも。(途中過程によっては無理かもだけど)
assert(cat != nullptr);
Animal4* thisAnimal = static_cast<Animal4*>(cat); // アップキャスト。①と同じものができる
LivingThing* thisLivingThing = static_cast<LivingThing*>(cat); // アップキャスト。②と同じものができる。
}
///ーー ポインタのコピー、クラス(構造体)のコピー ーー///
// ポインタをコピーすると、アドレスだけをコピーすることになり、参照先のメモリは1つなのに、そのアドレスだけ複数の変数が持つことになる。
// そのため、メモリをコピーしようとして、誤って意図せずアドレスをコピーしてメモリを共有してしまっている複数のポインタから、動的メモリの解放などを行うと、しばしばバグの原因になる。
int main() {
int* intPtr1 = new (nothrow) int(100);
int* intPtr2 = intPtr1;
printf("%d", *intPtr1);
delete intPtr1;
intPtr1 = nullptr;
delete intPtr2; // 既にメモリは解放されているのでエラー
intPtr2 = nullptr;
}
// これはポインタを内包するクラスや構造体にも同じことが言えて、デフォルトのままコピー系(コピコンやコピー代入演算子)を使うと、内包するポインタはアドレスをコピーすることになる。
// そのため、そのままだと deleteされずにメモリリークにつながるし、RAII設計のようにデストラクタ等で動的確保したメンバのポインタ変数を自動解放する設計にしているとエラーを吐く。
class OtherClass {
public:
int m_n;
OtherClass(int n) :m_n(n) {}
};
class MyClass {
private:
OtherClass* mObj;
public:
MyClass(int n) : mObj(new OtherClass(n)) {} // 動的確保
~MyClass() { delete mObj; } // デストラクタで自動解放
OtherClass* get_Obj() { return mObj; };
};
int main()
{
{
MyClass mc1 = MyClass(111);
MyClass mc2 = MyClass(222);
mc2 = mc1; // 複製を作りたい
printf("%d\n", mc1.get_Obj());
printf("%d\n", mc2.get_Obj()); // mc1.mObj と同じアドレスの同じメモリを参照
} // RAII設計なので mc1, mc2のデストラクタが同じメモリを delete しようとしてエラー & new された mc2.mObj のメモリは永遠に deleteされずメモリリーク
}
// これを避けるために、ポインタやポインタを含む型をコピーするときは、コピー系の働きを、ポインタの場合はアドレスではなく参照先をコピーするようにユーザー側が明示的に書き換えることが多い。
// 書き換える際には、= 代入演算子をオーバーロードしたり、コピーコンストラクタを明示的に定義する(後トピックで後述)。
// またクラス(構造体)を引数に渡す時は、基本的には値渡しよりも、参照値を渡すポインタ渡し/参照渡しをする。
// 値渡しをすると、内包するメンバを全てコピーするのでメモリを圧迫する上、コピコンが発動して処理が重くなるし、上記の通り内部にポインタを含む場合にはコピコンが正しく定義されてないと不具合が生じやすいため。
///ーー 演算子のオーバーロード ーー///
// 演算式が a + b の時、+ は演算子(オペレータ)といい a と b がオペランドという。
// 実は演算子は形が違うだけで関数とかなり近い。オーバーロードもできるし、引数や返り値も設定できるし、クラス内でメンバ関数としての定義もできる。
// 演算子をオーバーロードすることで、ベクトルや複素数などの数学的に特殊な四則演算や、文字列クラスの足し引き、上記の「ポインタ・クラスのコピー」の問題解決などが可能になる。
// ただし演算子をオーバーロードする時には、混乱を避けるために組み込み型に備わっているものと似た意味にすべき。例えば、+ なら加算、> なら大なり比較、>> ならストリームなど。
// 演算子のオーバーロードの形:「戻り値の型 operator 演算子 (引数…) {}」
// この戻り値の型や引数がその演算子の中のどの位置のオペランドを意味するのかは演算子によって予め決まっており、また演算子によって異なったりするため、行うときは都度調べた方が良い。
// 演算子のオーバーロードは非メンバ関数とメンバ関数の両方で実現できる。
// 非メンバ関数として実現された演算子関数は、引数として指定した型がいずれかのオペランドの位置に当てはめられる場合に呼ばれる。しかし、メンバ関数として実現された演算子関数は、自分の型がオペランドの左端の位置に当てはめられる場合のみに呼ばれるため、複数のオペラントがある演算子の場合、左端のオペラントの位置にある型が自分の型でない時にオーバーロードした演算子関数ではない演算子関数が呼ばれてしまう。
class Vector2 { // 例:二次元ベクトルクラス
public:
double m_x;
double m_y;
public:
Vector2(double x = 0, double y = 0) :m_x(x), m_y(y) {} // 本当は暗黙的型変換を防ぐために explicit にする(後述)
Vector2 operator + (const Vector2& v) { // メンバ関数として + 演算子を定義。メンバ関数だと + 演算子では左のオペランドが自インスタンスとして暗黙的に指定されている。
return Vector2(m_x + v.m_x, m_y + v.m_y);
}
};
Vector2 operator - (const Vector2& vl, const Vector2& vr) { // 非メンバ関数として - 演算子を定義。非メンバ関数なので左右両方のオペランドの型を引数で指定する必要がある。
return Vector2(vl.m_x - vr.m_x, vl.m_y - vr.m_y);
}
int main() {
Vector2 a(1, 1), b(2, 2), c(3, 3);
a = b + c; // OK ➀
a = b + 10; // OK ➁
a = 20 + c; // エラー ③
// ➀ Vector2が引数の + 演算子関数は非メンバ関数で定義されていないが、+ 演算子の左のオペランドである b の型 Vector2 のメンバで + 演算子関数が定義されているので、それが呼ばれる。
// ➁ Vector2が引数の + 演算子関数は非メンバ関数で定義されていないが、+ 演算子の左のオペランドである b の型 Vector2 のメンバで + 演算子関数が定義されているので、それが呼ばれる。そしてそのメンバ + 演算子関数の右オペランドの引数は Vector2 なので、暗黙的に型変換するようにコンストラクタが自動で働き、右オペランドは Vector2(10, 0) で変換されて計算される。
// ③ Vector2が引数の + 演算子関数は非メンバ関数で定義されておらず、+ 演算子の左のオペランドである 10 の型 int のメンバで + 演算子関数が定義されていないので、デフォルトの + 演算子関数が呼ばれる。そしてデフォルトの + 演算子関数の右オペランドの引数は int とかなので、その型に暗黙的に型変換するようにコンストラクタ(かキャスト演算子)が自動で働こうとして右オペランドは (int)c となるが、 Vector2 から int へは変換できないためエラーとなる。
a = b - c; // OK ④
a = b - 10; // OK ⑤
a = 20 - c; // OK ⑥
a = 20 - 10; // OK ⑦
// ④ Vector2が引数の - 演算子関数は非メンバ関数で定義されているので、それが呼ばれる。
// ⑤ Vector2が引数の - 演算子関数は非メンバ関数で定義されているので、それが呼ばれる。そして - 演算子関数の右オペランドの引数は Vector2 なので、その型に暗黙的に型変換するようにコンストラクタが自動で働き、右オペランドは Vector2(10, 0) で変換されて計算される。
// ⑥ Vector2が引数の - 演算子関数は非メンバ関数で定義されているので、それが呼ばれる。そして - 演算子関数の左オペランドの引数は Vector2 なので、その型に暗黙的に型変換するようにコンストラクタが自動で働き、右オペランドは Vector2(20, 0) で変換されて計算される。
// ⑦これはオーバーロードした - 演算子は関係なく、デフォルトの - 演算子関数で 20 - 10 の計算が行われた結果の int 10 が、= 演算子の左オペランドの型である Vector2 にコンストラクタで暗黙的型変換される。
}
// これらの理由から、「基本的には演算子のオーバーロードは、非メンバ関数として実現する」べき。
// しかし、代入演算子の連続 a = b = c のように連続して演算子の結果を代入していくような演算子は、返り値に参照変数を返すように定義されることが多い(参照変数を返さなくもできるが、デフォルトの定義で参照変数を返しているものはそれを踏襲した方が混乱を避けれるのですべき)。これらの演算子は、自インスタンスの参照を返すために、クラス内でメンバ関数として実現される必要がある。
// 参照変数を返す演算子としては、代入演算子系(=, +=, -= など)、添え字[]、アロー ->、new などがある。
//---------------.hpp
class IntArray { // 例:整数配列のクラス
private:
int m_size;
int* pFirst;
public:
explicit IntArray(int size) : m_size(size) { pFirst = new int[m_size]; }
~IntArray() { delete[] pFirst; }
public:
IntArray& operator = (const IntArray& arr); // = 演算子のオーバーロード
IntArray& operator += (const IntArray& arr); // +=演算子のオーバーロード
IntArray& operator -= (const IntArray& arr); // -=演算子のオーバーロード
int& operator [] (int i); // [] のオーバーロード。
const int& operator [] (int i) const; // const オブジェクト用の [] のオーバーロード。演算子のオーバーロードは、const の有無によっても区別される(普通のメンバ関数と同じ)。
class Error_OverFlowAccess {}; // 添え字が範囲外の時のエラークラス
int size() const { return m_size; }
};
//---------------.cpp
IntArray& IntArray::operator = (const IntArray& arr) { // = 演算子のオーバーロード。生の代入演算子だけは、自己代入にするとバグるので対処する必要がある。
if (&arr == this) { return *this; } // 自分 = 自分 にしたときの対処
if (m_size != arr.m_size) {
delete[] arr.pFirst;
m_size = arr.m_size;
pFirst = new int[m_size];
}
for (int i = 0; i < m_size; i++) {
pFirst[i] = arr.pFirst[i];
}
return *this;
}
IntArray& IntArray::operator += (const IntArray& v) { // += 演算子のオーバーロード
printf("このクラスじゃ += は使えないよ");
return *this;
}
IntArray& IntArray::operator -= (const IntArray& v) { // -= 演算子のオーバーロード
printf("このクラスじゃ -= は使えないよ");
return *this;
}
int& IntArray::operator [] (int i) { // [] のオーバーロード。
if (i < 0 || i >= m_size) { throw Error_OverFlowAccess(); } // サイズが範囲外だと指定の例外投げる
return pFirst[i]; // 配列と同じように インスタンス名[] で要素にアクセスできる
}
const int& IntArray::operator [] (int i) const { // const オブジェクト用の [] のオーバーロード
if (i < 0 || i >= m_size) { throw Error_OverFlowAccess(); }
return pFirst[i];
}
//---------------
// 他にも色々演算のオーバーロードあるよ
// + - 演算子にはオペランドが1つのものと2つのものがあるので注意。
5 + 4; 5 - 4; // 演算の±
+4; -5 // 不等号表す±
// 等価・不等価演算子は便利なのでオーバーロードしよう。
// 等価・不等価演算子を用いると、インスタンスの全てのメンバ変数が等しいか否かを判定してくれるため、実装しているとインスタンスが同一か否かを判断でき非常に便利になる。
bool operator == (const Vector2& vl, const Vector2& vr) { // 等価・不等価演算子は非メンバ関数としてオバロ
return (vl.m_x == vr.m_x) && (vl.m_y == vr.m_y);
}
bool operator != (const Vector2& vl, const Vector2& vr) {
return !(vl == vr); // 大抵等価演算子の後に書くので、すでに等価演算子はオバロされているのでこの書き方でOK。
}
// 論理演算子 || や && などのオーバーロードはすべきではない。
// デフォルトの論理演算子では、短絡評価によって最適化されるが、ユーザーが定義した論理演算子では短絡評価が行われなくなるため。
///ーー キャスト演算子のオーバーロード・コンストラクタの暗黙的型変換・explicit ーー///
// キャスト演算子のオーバーロードは必ずメンバ関数として記載する。
// キャスト演算子には、デフォルトのもの、関数記法のもの、static_castやdynamic_castといったものなどいくつか種類があるが、オーバーロードすると、全て(多分)のキャスト演算子が影響される。
// キャスト演算子オーバーロードの形:「operator 型() const { retuen 型の値; }」 …… 関数の形で戻り値の型を指定しないが、必ず戻り値が必要。
class Boolean {
public:
enum class boolean { FALSE, TRUE };
private:
boolean b;
public:
Boolean(int x) : b(x == 0 ? boolean::FALSE : boolean::TRUE) {}
operator int() const { return (int)b; } // int 型へのキャスト演算子関数。
operator const char* () const { return b == boolean::FALSE ? "False" : "True"; } // const char* 型へのキャスト演算子関数。const の有無でもキャスト演算子関数の定義は分かれる。
};
int main() {
Boolean bo(1);
if ((int)bo) { printf("true."); } // int 型に変換可能。
printf(static_cast<const char*>(bo)); // const char* 型に変換可能。
if (bo) { printf(" I said it's true."); } // キャスト演算子を使わなくても、変換可能な場合は暗黙的に bo は (int)bo に変換される。
// この↑暗黙的な型変換は一見便利にも思えるが、暗黙的な型変換を許していると以下のような連続的な型変換が生じ、意図しない不具合を見逃すことに繋がってしまう。
std::cout << bo * 3.1415f << std::endl; // 誤って違う型を演算してしまった場合でも (float)((int)boo) * 3.1415f と解釈されてコンパイルが通ってしまい、本来意図していない挙動が見逃されてしまう。
}
// また、キャスト演算子による暗黙的な型変換だけでなく、コンストラクタによる暗黙的な型変換も存在する。
class Test2 {
public:
Test2(int a) {}
};
int main() {
Test2 test2 = true; // 誤って違う型で初期化してしまった場合でも (Test)((int)true) と解釈されてコンパイルが通ってしまう。
// 加えて、暗黙的な型変換でコンストラクタが呼ばれる場合は、コンストラクタが重たい処理なため明示的に型変換するよりも処理効率が落ちてしまう。
}
// この理由から、キャスト演算子やコンストラクタによる暗黙的な型変換は禁止して、変換する可能性のある型への型変換を網羅的にユーザー定義し、利用時には明示的に型変換することが推奨される。
// explict キーワードは、キャスト演算子関数やコンストラクタの先頭に付けることで、暗黙的な型変換を禁止できる。
class Test3 {
public:
explicit Test3(int a) {} // コンストラクタで暗黙的型変換禁止
explicit operator int() const { return 1; } // キャスト演算子で暗黙的型変換禁止
};
int main() {
Test3 tt = true; // エラー。暗黙的に型変換しない
Test3 tt = (Test3)true; // 明示的に型変換した場合はOK
Test3 t(10);
if (t) { printf("true"); } // エラー。暗黙的に型変換しない
if ((int)t) { printf("true"); } // 明示的に型変換した場合はOK
}
// これを踏まえて「演算子のオーバーロード」にあったような二次元ベクトルクラスを作ると以下のようになる
//---------------.hpp
class Vector2_2nd {
public:
double m_x;
double m_y;
public:
explicit Vector2_2nd(double x = 0, double y = 0) :m_x(x), m_y(y) {}
Vector2_2nd& operator = (const Vector2_2nd& v); // =演算子のオーバーロード
};
inline Vector2_2nd operator + (const Vector2_2nd&, const Vector2_2nd&); // +演算子のオーバーロード。以下 + 演算子で使う可能性のある型(今回はint)を網羅的に定義
inline Vector2_2nd operator + (const int& intl, const Vector2_2nd& vr); // + 演算子のオーバーロード (int + Vector2)
inline Vector2_2nd operator + (const Vector2_2nd& vl, const int& intr); // + 演算子のオーバーロード (Vector2 + int)
inline Vector2_2nd operator - (const Vector2_2nd&, const Vector2_2nd&); // -演算子のオーバーロード。以下 - 演算子で使う可能性のある型(今回はint)を網羅的に定義
inline Vector2_2nd operator - (const int& intl, const Vector2_2nd& vr); // - 演算子のオーバーロード (int - Vector2)
inline Vector2_2nd operator - (const Vector2_2nd& vl, const int& intr); // - 演算子のオーバーロード (Vector2 - int)
//---------------.cpp
Vector2_2nd& Vector2_2nd::operator = (const Vector2_2nd& v) { // = 演算子のオーバーロード。
if (&v == this) { return *this; }
m_x = v.m_x;
m_y = v.m_y;
return *this;
}
Vector2_2nd operator + (const Vector2_2nd& vl, const Vector2_2nd& vr) { // + 演算子のオーバーロード(Vector2 + Vector2)
return Vector2_2nd(vl.m_x + vr.m_x, vl.m_y + vr.m_y);
}
Vector2_2nd operator + (const int& intl, const Vector2_2nd& vr) { // + 演算子のオーバーロード (int + Vector2)
return Vector2_2nd(intl, 0) + vr; // ちょうど1個上で Vector2 同士の + 演算子の挙動を定義しているのでここで使ってもOK
}
Vector2_2nd operator + (const Vector2_2nd& vl, const int& intr) { // + 演算子のオーバーロード (Vector2 + int)
return vl + Vector2_2nd(intr, 0);
}
Vector2_2nd operator - (const Vector2_2nd& vl, const Vector2_2nd& vr) { // - 演算子のオーバーロード
return Vector2_2nd(vl.m_x - vr.m_x, vl.m_y - vr.m_y);
}
Vector2_2nd operator - (const int& intl, const Vector2_2nd& vr) { // - 演算子のオーバーロード (int - Vector2)
return Vector2_2nd(intl, 0) - vr;
}
Vector2_2nd operator - (const Vector2_2nd& vl, const int& intr) { // - 演算子のオーバーロード (Vector2 - int)
return vl - Vector2_2nd(intr, 0);
}
//---------------main.cpp
int main() {
Vector2_2nd a(1, 1), b(2, 2), c(3, 3);
a = b + c;
a = b + 10;
a = 20 + c;
a = b - c;
a = b - 10;
a = 20 - c;
printf("%d %d", (int)a.m_x, (int)a.m_y);
}
//---------------
///ーー インスタンス化(オブジェクト化) ーー///
// 構造体やクラスを使っているとインスタンス化を意識するようになるが、基本的に組み込み型も enum型も構造体型もクラス型もほぼ同じ仕組みになっている。
// 型とは、値を作る設計図を表しており、各型の定義(設計図の中身)を基に値を作る。そして作られた値は、変換されたり演算されたりして形を変えていく。
// そしてそのような値は、変数に格納することでメモリに保持する。そしてこの、型を基に作られた値が保持されたメモリ領域を、実態 or インスタンス or オブジェクトと呼ぶ。
// 組み込み型やクラスは書き方が少し違うので一見違って見えるけど、以下のように内部の処理は大体同じ感じになる。
// Kumikomi int { ~~ クラスみたいな int 型の設計図 ~~ 詳細は知らん~~ };
class A { A() { printf("Aクラスだよ"); } }; // Aクラス型の設計図
123; // int(123)と同じ。int型のコンストラクタが呼ばれて int型の 123 の値が生成される。インスタンス化はされないしメモリも食わない(一時オブジェクトを無視すれば)。
A(); // Aクラス型のコンストラクタが呼ばれて Aクラス型の 不定値が生成される(初期値で初期化されてないから)。インスタンス化はされないしメモリも食わない(一時オブジェクトを無視すれば)。
int nnn = 123; // int型のコンストラクタが呼ばれて int型の 123 の値が生成されて(ここまで上と同じ)、さらに生成された値が変数に格納されて int型分メモリ食う(インスタンス生成)
A aaa = A(); // Aクラス型のコンストラクタ呼ばれて Aクラス型の 不定値 が生成されて(ここまで上と同じ)、さらに生成された値が変数に格納されてAクラス型分メモリ食う(インスタンス生成)
///ーー 左辺値・右辺値 ーー///
// デフォルトの代入演算子 = において、左辺にも右辺にも配置できるもの、つまり「値を代入可能な変数」と同じ扱いのものを左辺値という。
// 逆に右辺にしか配置できないもの、つまり「変数に格納されていない生の値」と同じ扱いのものを右辺値という。
int main() {
int x = 10; // x は左辺値、10は右辺値
int y = x; // 左辺値は右辺にも配置可能
10 = y; // エラー。右辺値は左辺に配置不可能。
}
// 代入演算子、複合代入演算子(=と何かがくっついたもの)、前置インクリメント/デクリメント、添え字、ポインタ演算子(*a)、メンバ系のアクセス演算子、による結果は全て左辺値になる。
// 逆に、後置インクリメント/デクリメント、アドレス演算子(&a)、配列名による配列先頭アドレス、関数の返り値、上記以外の四則演算とか大小比較とかの演算子全部の結果は全て右辺値になる。
int main() {
int a = 10;
int b = 20;
int c[10];
int d[10];
Car classSample;
(a = b) = 30; // a = b の結果は左辺値なので OK:代入演算子
a += b = 30; // a += b の結果は左辺値なので OK:複合代入演算子
a + b = 30; // a + b の結果は右辺値なので NG:四則演算子
++a = 30; // ++a の結果は左辺値なので OK:前置インクリメント
a++ = 30; // a++ の結果は右辺値なので NG: 後置インクリメント
c[5] = 30; // c[5] の結果は左辺値なので OK:添え字
*c = 30; // *c の結果は左辺値なので OK:ポインタ演算子
&a = 0x9304; // &a の結果は右辺値なので NG:アドレス演算子
c = d; // c の結果は右辺値なので NG:配列名による配列先頭アドレス
classSample.set_fuel() = 30; // classSample.set_Num()は左辺値なので OK:メンバ系のアクセス演算子。
// classSample-> こういうポインタのやつも左辺値:メンバ系のアクセス演算子
abs(-1000) = 10; // abs(x) は右辺値なので NG:関数の返り値
}
///ーー 一時オブジェクト・右辺値参照 ーー///
// 実は、変数に格納されてない、メモリに保持されない値(つまり右辺値)は、全てCPU上で保持される訳ではない。小さいものならCPU内部で一時的に保持されるが、大きめのクラス等の値の場合はスタック領域が使われる(管理を考える際は、右辺値は全てスタック領域に一時保持されている認識でOK)。
// つまり、一時的にメモリ領域に保持される → 一時的にインスタンス(オブジェクト)化される ため、右辺値(が保持されたメモリ)は「一時オブジェクト」といい、寿命はごく短く大抵の場合は式の終わり(一文のセミコロン;まで)ですぐに開放される。
// この一時オブジェクトが保持される寿命の長さは式などによって多少異なり、例えば例外処理だと、try{}中に throwした値は catch{}内まで保持される。
// この特徴を用いて、一時オブジェクト(右辺値)を計算過程などで瞬時的にしか使わない時は、一時オブジェクトを変数に格納して新たにインスタンス化するよりも、その短い寿命が尽きるまでの間に参照渡しした方が、メモリの節約にもなるしコピー等の処理を挟まないので高速に行える。
void PrintfTempObj(int&& rightValue) {
printf("%d", rightValue);
}
void PrintfObj(int& leftValue) {
printf("%d", leftValue);
}
int main() {
123456; // インスタンス化されていないただの int型の値。こういうインスタンス化してない右辺値も、一時的にスタック領域に確保される(一時オブジェクト)。ただしすぐに解放される。
abs(-1000); // 関数の返り値も右辺値なので一時オブジェクトになる。
int a = 10;
int& b = a; // 参照渡しや参照変数 & は左辺値にしか使えない。 && とすることで、右辺値参照渡しや右辺値参照変数を使えるようになる。
int& c = 11; // エラー
int&& d = 12; // && とすると右辺値を参照するようになる一方、左辺値を参照できなくなる。(ただこの使い方だと格納した d が mai()中生き続けるので、一時オブジェクトを main内までメモリに保持し続けることになり、結果 int型の変数を新しく作るのと変わらない)
int&& e = a; // エラー
const int& f = a; // const 参照変数 とすると、値は変更できなくなるが、左辺値でも右辺値でも参照できるようになる。
const int& g = 13; // 右辺値もOK
double& h = a; // 参照変数は型が異なる値は、暗黙的な型変換が出来る型だとしてもエラーになる。
double&& i = 15; // 右辺値参照なら型が異なる値でも、普通の変数のように暗黙的な型変換はされる。
const double& j = a; // const 参照変数 でもOK。
// 普通は関数の引数として渡すときに使う。
PrintfTempObj(a++); // 右辺値参照渡しを使っているので、右辺値でも参照可能。この時の右辺値 a++(後置インクリメント)が使用しているメモリは この式の終わり;で解放される。
int aa = a++; // 右辺値参照渡しをせず普通に参照渡しする場合は、一度変数に格納してインスタンス化する必要がある。
PrintfObj(aa); // main()内でインスタンス化されてるので、この式が終わっても main()スコープが終わるまで aa のメモリは解放されない。右辺値参照の方がメモリの節約になる。
}
// ちなみに、右辺値参照変数は、値を代入できてるので「左辺値」。
// そのため、右辺値参照変数や右辺値参照渡ししたものを、さらに右辺値として扱いたい時は、右辺値参照変数や右辺値参照渡しを std::move()等で右辺値に変換する必要がある。
///ーー コピーとムーブ ーー///
// C++11以降では、代入演算子などが同じ記述でコピーかムーブのどちらかの働きをする。二つの働きは PCのファイル操作に近い。コピーはAの変数の中身をBの変数の中身に複製する。一方ムーブはAの変数の中身をそのままBの中身に移す(Aの中身は空になる)。つまりムーブでは、メモリ上のアドレスを移動するだけで複製する処理を挟まないので、コピーよりも高速でメモリも節約できる。ただし、ムーブを行う際にはムーブ元は空になるので、それ以降使用も操作もしないメモリを対象とする必要がある。
// 代入演算子がコピーの処理になるか、ムーブの処理になるかは、右辺の値の種類に依存して自動で決まるので注意。
// 代入演算子の右辺が左辺値の時はコピーになり、右辺が右辺値の時にはムーブになる。
int a = 10; // ムーブ
int b = a; // コピー
// 左辺値をムーブしたい時は std::move関数を使うことで可能になる。std::move関数は引数の左辺値を右辺値に変換する(ただし、引数の元々の寿命の長さは変わらない)。
int c = std::move(a); // 左辺値 a を右辺値にしてムーブ
// 、、上記が一応ムーブの概論だが、実際には C++では、明示的に定義されていないデフォルトのムーブ系(次トピックで後述)は単に全てのメンバ変数をコピーするだけで「デフォルトのコピー系と同じ」であり、移動元の中身が空になる処理は「ムーブ系の定義内でユーザーが明示的に記述する必要がある」(ここの内容は一つ後の特殊メンバ関数のトピックを読んだ後に読んで)。
// そして、ムーブ系を含む特殊メンバ関数は型の内部でメンバとして定義されているため、ポインタ変数ではなく普通の変数を対象としないとただのコピーと同じになる(特殊メンバ関数は型ごとに定義するが、同じ型でも普通の変数でのみ利用できて、ポインタ変数では利用できない)。
// ただし、スマートポインタはそのポインタの参照先のメモリを自動でムーブしてくれるので、普通の変数でなくポインタ変数もムーブの対象にできるし、ムーブ系の明示定義も必要ない。
// また、ムーブ後に移動元の中身を空にしたとしても、C++では宣言を取り消すことはできないので、移動元の変数はゼロ初期化などで空になったことを表現されるだけで不定値や0やそのままの値で残る。そのため、処理の高速化とメモリの節約は、ムーブ対象の内部のポインタ変数の参照先のサイズ分しか効果がない(ポインタのムーブはアドレスの受け渡しになるので、参照先には触れず、参照先はそのまま使い回せる)。
// まとめると、ムーブが発動するのは以下の2パターン ①-1:対象がポインタでない普通の変数で型内のメンバ変数にポインタ変数が存在して、①-2:そのメンバのポインタ変数がスマポか、型内でムーブ系が明示定義されている時。②:対象がスマートポインタ変数の時。であり、処理の高速化とメモリ節約はポインタの参照先分のみ効果がある。そして、スマポを使わない時はムーブ系特殊メンバ関数の明示定義が必要になる。デフォルトで用意されているムーブ系は deleteを使ってその型のインスタンスがムーブされないことを保証する時くらいにしか使えない。
struct NoMove {
int* ptr_n;
int n;
NoMove(int a) : ptr_n(new int), n(a) {}
~NoMove() { /* ポインタのシャローコピーでバグるので delete は省略。コピコンめんどい */ }
};
struct HaveMove {
int* ptr_n;
int n;
HaveMove(int a) : ptr_n(new int), n(a) { }
~HaveMove() { /* ポインタのシャローコピーでバグるので delete は省略 */ }
HaveMove(HaveMove&& r) : ptr_n(r.ptr_n), n(r.n) { // ムブコンで 右辺のインスタンスのメンバ変数を明示的にゼロ初期化。
r.ptr_n = nullptr;
n = 0;
}
HaveMove& operator=(HaveMove&& r) { // ムーブ代入演算子で 右辺のインスタンスのメンバ変数を明示的にゼロ初期化。
if (&r != this) {
this->ptr_n = r.ptr_n;
this->n = r.n;
r.ptr_n = nullptr;
r.n = 0;
}
return *this;
}
};
struct HaveSmartPtr {
std::unique_ptr<int> sp_int;
int n;
HaveSmartPtr(int a) : sp_int(new int), n(a) { }
};
// ブレークポイントを使ってステップ実行しながら、それぞれの変数に何が格納されているか確認すると分かりやすい。
int main() {
// ムーブが発動するのは以下の2パターン
// ①-1:対象がポインタでない普通の変数で型内のメンバ変数にポインタ変数が存在して、①-2:そのメンバのポインタ変数がスマポか、型内でムーブ系が明示定義されている時。
// ②:対象がスマートポインタ変数の時。
int x = 10;
int y = std::move(x); // 対象が普通の変数だが、int 型内にはポインタのメンバ変数がないので、ムーブ不成立(コピーと一緒の働きになる)。
NoMove noMove1(10);
NoMove noMove2 = std::move(noMove1); // 対象が普通の変数で型内にポインタのメンバがいるが、NoMove 内でムーブ系が明示定義されてなく、またメンバのポインタ変数はスマポでないので不成立。
HaveMove haveMove1(20);
HaveMove haveMove2 = std::move(haveMove1); // 対象が普通の変数だが型内にポインタのメンバ変数がおり、NoMove 内ではムーブ系が明示定義されているのでムーブ成立。
HaveSmartPtr haveSmartPtr1(30);
HaveSmartPtr haveSmartPtr2 = std::move(haveSmartPtr1); // 普通の変数だが型内にポインタのメンバ変数がおり、そのポインタ変数はスマポなのでムーブ成立。
HaveMove* ptr_haveMove1 = &haveMove2;
HaveMove* ptr_haveMove2 = std::move(ptr_haveMove1); // 対象がポインタ変数なのでムーブ不成立。
std::unique_ptr<int> sp_int1(new int(40));
std::unique_ptr<int> sp_int2 = std::move(sp_int1); // 対象がスマポなのでムーブ成立
}
// RAII設計(デストラクタでの自動解放設計)で、ムーブ時に移動元を明示的にゼロ初期化しないでバグる例
struct Bar1 {
int* ptr_n;
int n;
Bar1(int a) : ptr_n(new int), n(a) {
printf("コンスト\n");
}
~Bar1() {
printf("デスト\n");
delete ptr_n;
ptr_n = nullptr;
}
// 本当は、ムーブするなら、ムブコンとムーブ代入演算子内で右辺のインスタンスのメンバ変数を明示的にゼロ初期化する必要がある。
};
int main() {
Bar1 bar1(1);
Bar1 bar2(2);
bar2 = std::move(bar1); // デフォルトのムブコンは、デフォルトのコピコンと同じなので、内部メンバのポインタ(参照先が同じアドレス)もコピーしちゃって、所有権を2つもポインタが持つ。
} // デストラクタで deleteが重複してエラー吐く。
// ムーブ時に移動元を明示的にゼロ初期化するとバグを防げる(最低でもポインタに nullptrの代入は必要)
struct Bar2 {
int* ptr_n;
int n;
Bar2(int a) : ptr_n(new int), n(a) {
printf("コンスト\n");
}
~Bar2() {
printf("デスト\n");
delete ptr_n;
ptr_n = nullptr;
}
Bar2(Bar2&& rBar) : ptr_n(rBar.ptr_n), n(rBar.n) { // ムブコンで 右辺のインスタンスのメンバ変数を明示的にゼロ初期化。
rBar.ptr_n = nullptr;
n = 0;
}
Bar2& operator=(Bar2&& rBar) { // ムーブ代入演算子で 右辺のインスタンスのメンバ変数を明示的にゼロ初期化。
if (&rBar != this) {
this->ptr_n = rBar.ptr_n;
this->n = rBar.n;
rBar.ptr_n = nullptr;
rBar.n = 0;
}
return *this;
}
};
int main() {
Bar2 bar1(1);
Bar2 bar2(2);
bar2 = std::move(bar1); // ムーブ後は右辺のインスタンスのメンバ変数のポインタは nullptr。
} // デストラクタで deleteは重複しない
///ーー 型に内包される、コンストラクタ系と代入演算子たち(特殊メンバ関数) ーー///
// 全ての型は、宣言(初期化)時や代入演算子の使用時に、式の形が違うと処理が分かれる。下の式たちは似てるように見えるが、型内で全く別の関数が定義されており、全く別の処理フローが流れている。
// ○○コンストラクタの名前がついてるのはインスタンスを宣言(初期化)した時の処理、○○代入演算子はそれ以外の場面で代入演算子を用いた時の処理を意味している。
int main() {
int num1; // コンストラクタ(デフォルト)でインスタンス化
int num2 = 10; // ムーブコンストラクタでインスタンス化(右辺値で初期化)
int num3 = num2; // コピーコンストラクタでインスタンス化(左辺値で初期化)
num2 = 20; // ムーブ代入演算子でムーブ。
num3 = num2; // コピー代入演算子でコピー。
int* num4 = new int(20); // コンストラクタで生成した値を動的確保でインスタンス化
} // メモリ開放時に int型のデストラクタ発動。 num1 ~ num4 の4つ
// この宣言(初期化)時や代入演算子使用時に呼ばれる関数は特殊メンバ関数といい、コンストラクタ, コピーコンストラクタ, ムーブコンストラクタ, コピー演算子, ムーブ演算子, デストラクタがある。
// 以下のように明示的に定義できる。(定義の中身はめんどいから省略)
class ExpConsts
{
private:
int m_a;
int m_b;
Car m_car;
public: // 特殊メンバ関数は全て public に入れる。
// コンストラクタ「クラス名(...){}」:初期化が無い変数の宣言時 or クラス名(...)による呼び出し で呼ばれる。前者はインスタンス化され、後者の呼び出しでは右辺値を生じる。
ExpConsts() {}; // デフォルトコンストラクタ:引数がないコンストラクタ
// コンストラクタは引数を付けてオーバーロードできる。
explicit ExpConsts(int a) {} // 暗黙的型変換を意図していないコンストラクタには、必ずexplicit(明示的)をつける。「演算子のオーバーロード」で先述。
explicit ExpConsts(int* a, int b) : m_a(a), m_b(b) m_car(0, 0) {} // コンストラクタは {} の前に「 : メンバ変数(初期値)」でインスタンス生成時にメンバ変数の初期値を直接決めれられる(初期化子による初期化)。初期化子による初期化は、()内に連ねて書くとその変数の型内のメンバ変数も初期化できる(例えばここではCarクラスの m_fuel, m_distance を初期化してる)。
// 初期化子による初期化は、コンストラクタの {} ブロック内で代入するよりも高速に行われる。 {} 内による代入では、メンバ変数がその型のコンストラクタで一度初期化された後にこのクラスのコンストラクタの引数を代入する処理になるが、初期化子による初期化は普通の変数の初期化と同じでメンバ変数の宣言と同時にその型のコンストラクタが呼ばれて値が設定されるので、処理が短い。➀
// また、初期化子による初期化を行うと、そのメンバ変数のコンストラクタにも順繰りに、コンストラクタの引数として与えられていくので注意。➁
// また、staticメンバ変数はインスタンス生成時のコンストラクタが起動する前に存在するため、初期化子リストでは初期化できない。
{ // このスコープ内だけは急にコンストラクタの例。
struct A {
int m_N;
A() { printf("Aのデフォコンだよ\n"); }
A(int n) : m_N(n) {}
};
struct B {
A m_A;
B() { printf("Bのデフォコンだよ\n"); }
B(A a) : m_A(a) {}
};
int main() {
// ➀
A a1;
B b1; // 初期化子による初期化がないと内部のメンバ変数が宣言されるので、メンバ変数の m_A の型であるAクラス型のデフォコンが呼ばれる
B b2(a1); // 初期化子による初期化があると内部メンバ変数の宣言がされずにそのまま値が代入されるので、Aクラス型のデフォコンは呼ばれない
// ➁
// 初期化子による初期化があるので、メンバ変数の m_A の型である「Aクラス型に int 12 がキャスト」されてBクラス型のインスタンス生成 ではない
// 初期化子による初期化があり、それにより初期化される変数の型であるAクラス型にも、初期化子による初期化があるので、Aクラス型のメンバ変数 m_N がまず int 12 で初期化子による初期化され、次に、それで生じたAクラス型のインスタンスで m_A が初期化される。つまり、Aクラスの初期化子による初期化 → Bクラスの初期化子による初期化 の様に「最下層から順に初期化されていく」
B bb = B(12);
printf("%d\n", bb.m_A.m_N);
}
}
// デストラクタ「 ~クラス名(){}」:インスタンスの破棄時 に呼ばれる。
virtual ~ExpConsts() {} // 大抵デストラクタ内で動的に確保したメンバ変数のメモリ開放とかを行う。
// コンストラクタとデストラクタだけ継承によるオーバーライドの仕組みがかなり変わってる。
// コンストはデフォで子も親も両方呼ばれるし virtual をつけたら?、デストはデフォで親しか呼ばれないし virtual をつけると両方呼ばれる。
// virtual つけない時:親のコンスト→子のコンスト→親のデスト(子デスト無し)。 つけた時:親のコンスト→子のコンスト→子のデスト→親のデスト。
// ここもうちょっとちゃんと調べよう
// コピーコンストラクタ「クラス名(const クラス名 & ){}」:宣言時に代入演算子と左辺値で初期化した時に呼ばれる。関数内の thisが左辺、引数が右辺のインスタンスを示す。
ExpConsts(const ExpConsts& expR) : /* 基本初期化子を使う */ {} // 一応コピコンも代入演算子みたいに自分で自分を初期化したときにバグるけど、よほど変な書き方しないと不可能なので省略
// ムーブコンストラクタ「クラス名(クラス名 && ){}」:宣言時に代入演算子と右辺値で初期化した時に呼ばれる。関数内の thisが左辺、引数が右辺のインスタンスを示す。
ExpConsts(ExpConsts&& expR) : /* 基本初期化子を使う */ { /* 右辺のムーブ元のメンバ変数を空にする(初期化する)処理 */ }
// コピー代入演算子「クラス名 & operator=(const クラス名& 引数){}」:宣言時の初期化以外で、代入演算子と左辺値を使った時に呼ばれる。関数内の thisが左辺、引数が右辺のインスタンス。
ExpConsts& operator=(const ExpConsts& expR) { if (&expR != this); return *this; } // 代入演算子は代入元が自身じゃないことのチェックと、左辺(自身)のインスタンスの参照を返す必要あり。
// ムーブ代入演算子「クラス名 & operator=(クラス名&& 引数){}」:宣言時の初期化以外で、代入演算子と右辺値を使った時に呼ばれる。関数内の thisが左辺、引数が右辺のインスタンスを示す。
ExpConsts& operator=(ExpConsts&& expR) { if (&expR != this); /* 右辺のメンバ変数を空にする処理 */ return *this; } // 代入演算子系は自身じゃないチェックと、自インスタンス参照を返す。
// 代入演算子系以外の特殊メンバ関数には返り値がないので、処理結果の成功失敗をT/F等の返り値で表現できない。そのため、失敗する可能性のある処理は、特殊メンバ関数の外で行うか、何かしらのバグ対応を付ける必要がある(C++はコンパイル型なので、環境依存でコンストラクタとかがバグったりする可能性もある)。
// 例えば、コンストラクタ部分で直に初期化処理を書かずに関数を挟み、コンストラクタ後にコンストラクタが働いていない場合に、初期化を手動で行う補完処理を追加するなど。
class Player {
int life;
bool isConstWorkProperly = false;
Player(int n) :life(n) { InitPlayer(); isConstWorkProperly = true; }
void InitPlayer(); // コンストラクタ後に確認して、変になってたら手動でもう一度動かす
};
};
// また、これら特殊メンバ関数は、明示的に定義しなくても、全ての型で初めから暗黙的にメンバ関数として定義されている。
class ImpConsts // 以下の感じで暗黙定義されてる(厳密な定義ではなく大体のイメージ)
{
public:
ImpConsts() { /* 何もなし */ }; // コンストラクタ(デフォルト)
~ImpConsts() { /* 何もなし */ }; // デストラクタ
// 引数の種類や 引数中の constの位置(あるいは有無)が人によって違って分かりにくい時があるから、コピー系とムーブ系は軽くコメントで明示しよう
ImpConsts(const ImpConsts& impR) { /* 右辺のインスタンスの全てのメンバ変数の値を自インスタンスにコピー */ }; // コピコン
ImpConsts(ImpConsts&& impR) { /* 右辺のインスタンスの全てのメンバ変数の値を自インスタンスにムーブ */ }; // ムブコン
ImpConsts& operator=(const ImpConsts& impR) { /* 右辺の全てのメンバ変数の値を左辺にコピー */ }; // コピ代入
ImpConsts& operator=(ImpConsts&& impR) { /* 右辺の全てのメンバ変数の値を右辺にムーブ */ }; // ムブ代入
};
// ただし、特定の条件下では、これらの特殊メンバ関数は暗黙的に削除される。
// ・デフォルト以外のコンストラクタを定義した場合には、デフォルトコンストラクタは削除される。
// ・デストラクタを明示的に定義した場合には、ムーブ系(ムブコンとムーブ代入演算子)は削除され、コピー系は非推奨になる(今後のアプデで削除される)。デストラクタが定義されているとデストラクタ内で解放処理を行う可能性が高いが、暗黙定義の内容のムブコンやコピコンが発動すると、リソースを共有したポインタが発生する可能性があり、その際に deleteを重複して呼び出す危険性があるため。
// ・1つでもコピー系かムーブ系を定義した場合には、デフォルトのコピー系とムーブ系は全て削除される。コピーやムーブに対応する型の場合、暗黙定義の内容では使い物にならない可能性が高いため。
// だが、これらの暗黙的削除は分かりにくいこともあるので、正直明示的に deleteで削除したほうが意図が伝わりやすい、、。
// また、暗黙定義を削除したい時は deleteを、明示的に暗黙定義と同じ定義をしたいときは defaultを特殊メンバ関数に代入できる。
class Singleton // よくある静的確保によるシングルトン実装
{
private:
Singleton() = default;
~Singleton() = default;
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
static Singleton& get_instance() { // staticローカル変数によるアクセスしか許可しないため、全ての特殊メンバ関数を private化 または delete。
static Singleton instance;
return instance;
}
};
// 特殊メンバ関数は型ごとに定義するが、同じ型でも普通の変数でのみ利用できて、ポインタ変数では利用できない
int main() {
ExpConsts ex1; // コンストラクタ
ExpConsts ex2 = ex1; // コピコン
ex2 = std::move(ex2); // ムブコン
ExpConsts* ptr_ex1; // コンストラクタは生じない
ExpConsts* ptr_ex2 = new ExpConsts(); // コンストラクタのみ生じて、コピコンは生じない。
ptr_ex1 = std::move(ptr_ex2); // ムブコンは生じない
}
// ちなみに、書き方的にコピコンとムブコンは、初期化の時だけじゃなくて、普通の関数みたいにしても使うこともできる
int main() {
ExpConsts ex1;
ExpConsts ex2;
ExpConsts ex3 = ex1; // コピコン:宣言時の初期化の形
ExpConsts ex4(ex2); // コピコン:関数の形
ExpConsts ex5 = std::move(ex3); // ムブコン:宣言時の初期化の形
ExpConsts ex6(std::move(ex4)); // ムブコン:関数の形
}
// また、右辺値にもコンストラクタとデストラクタは発動する
int main() {
ExpConsts(); // ただの値。だけどコンストラクタとデストラクタは発動してる
}
// コンストラクタ系とか、代入演算子系って、継承されるとどういう感じでで動くようになるんだ?コンとかムブコンとかコピコンとかデストとか。
// デストラクタが virtual 必要なのとかとあわせて色々調べなおそう
///ーー 静的領域上の変数の初期化とコンストラクタが呼ばれる順序 ーー///
// 静的領域の場合、プログラム起動時にメモリが全部まとめて獲得され、このタイミングで全てゼロ初期化される。その後、処理系が定める順序でコンストラクタが呼出されるが、必ずしも必要に応じて呼ばれるわけではないので注意。
// 静的変数が初期化される順序は、同じコンパイル単位に属するものは変数を定義した順序だが、異なるコンパイル単位(ファイル分けしてる時とか)の静的変数間の順序は規定されておらず、ビルドする時の手順でも変わることも普通にある。
// このため、静的変数を初期化する時に他のファイル等の静的変数の値を使っていると動作が保証されないため、静的変数の初期化時には他の静的変数の値を使わない設計にする必要がある(代入演算子での初期化でも、クラスだとコンストラクタでの初期化でも)。
// メンバ変数のコンストラクタが呼ばれる時の挙動
// メンバ変数はそれを内包するクラスがインスタンス化される時に宣言&初期化されるが、宣言後の値の代入ではなくコンストラクタの初期化子リストによって初期化されるときは、メンバ変数は型のコンストラクタに初期子の値を引数にして初期化される(型のデフォルトコンストラクタで初期化されて、その後に代入される訳ではない)。そのため、初期化子リストによる初期化は代入による初期化よりも代入の処理が省けて軽くなる。
// メンバ変数の、コンストラクタでの初期化子リストによる初期化は、初期化子の順番ではなく、メンバ変数を定義した順序で初期化されるので注意。
struct Foo
{
int mData;
Foo() : mData(123) { printf("デフォコン\n"); }
Foo(int iData) : mData(iData) { printf("Foo(%d)\n", mData); }
~Foo() { printf("~Foo(%d)\n", mData); }
};
struct Bar
{
Foo mFoo1;
Foo mFoo2;
Bar() : mFoo1(1), mFoo2(2) { } // Foo mFoo1(1); Foo mFoo2(2); というコンストラクタで初期化したのと同じ挙動になる。Foo mFoo1; の後に→ mFoo1.mData = 1; ではない。
Bar(bool) : mFoo2(20), mFoo1(10) { } // mFoo2、mFoo1の順ではなく、mFoo1, mFoo2の順で初期化される
};
int main()
{
Bar bar1;
Bar bar2(true);
Foo foo123; // Foo(1) → Foo(2) → Foo(10) → Foo(20) → デフォコン
} // ~Foo(123) → ~Foo(20) →~Foo(10) →~Foo(2) →~Foo(1)
// クラスのコンストラクタが呼ばれる順序
// 親と自分と自分メンバで異なるので注意。
// 親クラス → 自メンバ変数 → 自クラスの順。デストラクタはこれと逆順で、自クラス → 自メンバ → 親クラスの順。
struct Foo
{
int mData;
Foo() : mData(2) { printf("デフォコンmFoo2\n"); }
Foo(int iData) : mData(iData) { printf("Foo(%d)\n", mData); }
~Foo() { printf("~Foo(%d)\n", mData); }
};
struct Bar
{
Foo mFoo1;
Foo mFoo2;
Bar() : mFoo1(0) { printf("mFoo1しか初期化してないのに、mFoo2の後になるんかい。\n"); } // 自コンストラクタは自メンバの後なので、初期子リストで初期化してなくても mFoo2の後
};
int main()
{
Bar bar1;
}
///ーー return の挙動と RVO/NRVO、コピコン/ムブコンが生じる注意点 ーー///
// コピコンとムブコンは ①変数を宣言時に初期値を設定する時、②関数に引数として与える時、に加えて ③関数が返り値を返す時 にも生じる。(前2つに比べて3つ目は一見変数を初期化しないため直感的に分かりにくいが、関数が返り値を返す時はその返り値の型内でコピコンかムブコンを定義してないとエラーを吐くので注意!!)。
// 関数内で return で指定する返り値(関数内返り値とする)と、関数から呼び出し元に返る返り値(関数外返り値とする)は実は別物で、return 文の処理で、関数外返り値を関数内返り値で初期化する処理が生じる(そしてそれが最終的に右辺値になる、多分)。
// この時の代入の仕方には、コピー初期化とムーブ初期化があり、それぞれコピコンとムブコンが生じる。どちらが生じるかは、return で指定する関数内返り値に依存する。
// また、特定の条件の時には、初期化の処理を挟まず、関数内で return で指定された返り値がそのまま関数外返り値として用いられる時がある(全部これすればいいのに何か出来ないっぽい)。これを RVO/NRVOといい、ムブコンが生じる条件で最適化の結果勝手に生じることがある。
// 実際の挙動は以下のようになる。
// ① 関数内返り値が、return 文の後にメモリが破棄されるローカル変数の場合は、RVO/NRVO されるか、されない時はムーブになる。また std::moveで明示的に指定すると強制的にムーブになる。
// ② 関数内返り値が、return後もメモリが有効な staticな変数か、ローカル内で動的確保したポインタ変数の場合はコピーされる。また std::move() で明示的に指定すると強制的にムーブされる。
struct Bar
{
int* ptr_n;
int n;
Bar(int a) : ptr_n(new int), n(a) {
printf("コンスト\n");
}
~Bar() {
printf("デスト\n");
delete ptr_n;
ptr_n = nullptr;
}
Bar(const Bar& rBar) : ptr_n(rBar.ptr_n), n(rBar.n) {
printf("コピコン\n");
}
Bar& operator=(const Bar& rBar) {
printf("コピ代入\n");
if (&rBar != this) {
this->ptr_n = rBar.ptr_n;
this->n = rBar.n;
}
return *this;
}
Bar(Bar&& rBar) : ptr_n(rBar.ptr_n), n(rBar.n) { // ムブコン
printf("ムブコン\n");
rBar.ptr_n = nullptr;
n = 0;
}
Bar& operator=(Bar&& rBar) { // ムーブ代入演算子
printf("ムブ代入\n");
if (&rBar != this) {
this->ptr_n = rBar.ptr_n;
this->n = rBar.n;
rBar.ptr_n = nullptr;
rBar.n = 0;
}
return *this;
}
};
Bar Local()
{
Bar lBar(10);
return lBar; // return後にメモリが破棄されるローカル変数は RVO/NRVO がされるか、されない時は自動でムーブになる。
}
Bar Local_Move()
{
Bar lBar(10);
return std::move(lBar); // std::move() で明示的に指定すると RVO/NRVO されるときもムーブになる。
}
Bar Stay()
{
Bar* ptr_bar = new Bar(20);
return *ptr_bar; // return後もメモリが有効な staticな変数、ローカル内で動的確保したポインタ変数はコピーされる。
}
Bar Stay_Move()
{
Bar* ptr_bar = new Bar(20);
return std::move(*ptr_bar); // std::move() で明示的に指定するとムーブされる。
}
int main()
{
Local();
Local_Move();
Stay();
Stay_Move();
}
///ーー 迷いやすい代入系全般のまとめ・コピー・ムーブ・参照・右辺値参照 ーー///
// 普通の式の場合、= の右辺が右辺値の場合ムーブ、左辺値の場合コピーが生じる。ムーブは右辺の中のポインタ系を移動させて他のは複製する。コピーは右辺の全てを複製する。
// = の左辺が参照変数の時、左辺と右辺はエイリアスになるだけ。
// = の左辺が右辺値参照変数の時、右辺のすぐ死ぬ一時オブジェクトであったはずの右辺値は、寿命が左辺破棄まで伸び、左辺はその少し長生きになった一時オブジェクトのエイリアスになる。
// 関数の引数になる場合のみ、= の右辺が右辺値でも左辺値でも、コピーが生じる、、?(要チェックかも。たしか左辺値の時にコピコンが生じるだけだったような、、ムブコンは生じなかったような、、。でも引数に右辺値ぶち込む時があって、例えば m_queue.push(std::move(val)) とかよくありそうな、、シンプルにコンテナ系が右辺値参照を引数にしたオバロ持ってるのかな?)が、関数の引数でも左辺が参照変数や右辺値参照の場合については引数でない場合と全く同じ。
// ムーブ系の関数の引数に右辺値参照が使われてはいるが、ムーブと右辺値参は同じものでは全くない。あくまでもムーブは「 = の右辺の中のポインタ系を移動させて他のは複製する」ことを機能としており、その発動に「同じ = 使っているコピーと区別するために右辺値と右辺値参照を使っている」だけ(コピーは区別するために左辺値と参照使ってる。別にコピコンとかコピ代入でもムーブ表現できる)。
// 左辺値も、std::move()を使うと右辺値として扱えるようになり、これを使って変数をコピーかムーブかで切り替えられる。(これもどういう扱いになるのかちゃんと調べないとな、、寿命そのままで判定だけ右辺値になるのか、寿命もスコープ内だけの短くなるのか、、。でも少なくとも右辺値扱いされてムーブされるから、中にポインタ系持ってたら中身移動して使い物にならなくはなるな、、。)
// あれ?てか返り値でもコピコンが生じちゃうなら、返り値ってほぼかならず std::move とかで右辺値にしてムーブした方が良くねーか?関数に返り値を返させる度にでかい値コピーしてたらあほちゃう?
///ーー スマートポインタ ーー///
// 動的確保の際に生じる delete忘れや deleteの重複を防ぎ、また種類を使い分けることでプログラムの意図も表現できる、改良されたポインタのこと。
// 動的確保したメモリをスマートポインタに格納して所有権(動的に確保したメモリを解放できる権利)を渡し、スマポの種類で所有権の共有の可不可を管理できる。
// また、所有権を持つスマポが全て破棄されるとスマポの型内で暗黙定義されたディストラクタによって、自動的に確保したメモリを解放してくれるので、解放を忘れてもスマポの破棄のタイミングで自動で解放してくれる(明示的な解放も可能)。
// また、デフォルトでムーブ時にポインタに nullptrを代入してくれる。つまり、ムーブするポインタの型内で明示定義しなくても、ムーブ系の定義を自動で行ってくれる。そのため、組み込み型でもムーブを実装できる!!
// また、get() 関数などでスマポから生のアドレスを取得できるが、ポインタとスマポを同時に使用すると所有権の管理がめちゃくちゃになってスマポの意味が無くなるので、スマポを使うときは生のポインタを使うべきではない。
// 所有権を1つに限定する「 std::unique_ptr<T> 」
// このポインタが指すメモリの所有権を、このポインタのみが持つことを保証する。
// このポインタはコピーが出来ない代わりに、ムーブによって所有権を移動できる。しかも、明示的にムーブを定義しなくても、ポインタ変数だけは nullptrで初期化してくれるので deleteの重複によるエラーが生じない(ただ、ポインタ変数以外はゼロ初期化されずに放置、まぁどっちみち以降使わないインスタンスだからポインタ変数だけ再利用できればOK)。
// また、このスマポは通常のポインタくらい処理速度が早く、配列の形にでき、delete以外の解放機構を指定することもできるので最&高の代物。利用には #include <memory> が必要。
class UniquePtr {
public:
std::unique_ptr<int> sp_int1;
int int2 = 20;
UniquePtr() {};
UniquePtr(int a) : sp_int1(new int(a)) {}
};
int main() { // ブレークポイント設定して、ステップ実行しながら各変数とポインタが指すメモリを確認するとわかりやすいよ。
UniquePtr uniquePtr(10);
UniquePtr uniquePtr2(uniquePtr); // メンバ変数のスマポがコピー出来ないのでエラー:コピコン
UniquePtr* uniquePtr3 = new UniquePtr();
*uniquePtr3 = uniquePtr; // メンバ変数のスマポがコピー出来ないのでエラー:コピ代入演算子
*uniquePtr3 = std::move(uniquePtr); // メンバ変数のスマポはムーブされてムーブ元は nullptrになる(クラスインスタンスがムーブされると、内部のメンバもすべてムーブが走る?)
if (uniquePtr.sp_int1 == nullptr) { printf("ムーブ元 nullptrやん"); }
delete uniquePtr3; // インスタンスが破棄されるとメンバ変数のスマポも破棄されるので、スマポ型内で暗黙定義されたデストラクタによって、自動的にポインタ先の確保したメモリを解放する。
uniquePtr3 = nullptr;
}
// 使い方の注意点
int main() {
// unique_ptr ポインタは生成手段が限られる
std::unique_ptr<int> up_int1(new int(10)); // コンストラクタの引数として、動的確保したメモリのアドレスを指定するか
std::unique_ptr<int> up_int2; // スマポは宣言時にデフォルトで nullptrを代入してくれるので、nullptrによる初期化省ける。
up_int2.reset(new int(10)); // 先に宣言だけしたものに、reset関数を使って後から代入するか
std::unique_ptr<int> up_int3 = std::make_unique<int>(10); // C++14以降なら make_unique関数が使える。
// メモリ開放は、自動解放以外にも unique_ptr.reset() で明示的に解放することも可能。
up_int1.reset();
// swap関数で管理するメモリが入れ替わる
std::swap(up_int2, up_int3);
// ムーブを行うと、移動先がすでに別のメモリを管理していた場合には、元々移動先にあった古いメモリを自動的に解放してくれる。
up_int2 = std::move(up_int3); // up_int2 の参照メモリは解放される(多分?確認して)
// アドレスを返したい時には、& ではなくget関数を使う。
int* pint;
pint = up_int3.get(); // ポインタに代入すると、普通にアドレスを値コピーしたみたいに同じ参照先を指す(所有権を持つ)ポインタが、スマポの所有権管理に無関係に増えちゃうので注意!
// 所有権を放棄してアドレスを返したい時は、release関数を使う
pint = up_int3.release(); // ポインタに代入すると、ムーブしたみたいに所有権が移る。
delete pint; // このようにポインタに代入する場合は、スマポでなくなるので、メモリの解放自体は自分で行う必要がある
}
// ユーザー定義の deleteでない解放機構を使いたい時は、std::unique_ptr<T> の Tの後ろの第二引数に deleterを設定する
int* malloc_int_from_storage() { /* 特定メモリブロックint型のリソースをに確保する */ }; // 今回は演算子ではなく関数なので malloc/freeと表現
void free_int_from_storage(int* ptr) { /* 特定メモリブロックからint型のリソースを解放する */ };
struct deleter_for_storage { // free_int_from_storageを使ってメモリを解放する関数オブジェクト(インスタンス名()で発動する関数)を定義。
void operator()(int* ptr_) {
free_int_from_storage(ptr_);
}
};
int main() {
std::unique_ptr<int, deleter_for_storage> ptr(malloc_int_from_storage()); // 第二引数で、deleterを指定
return 0; // メモリ開放時には、deleteではなく free_int_from_storageが呼ばれる
}
// 所有権を複数で共有する「 std::shared_ptr<T> 」
// このポインタが指すメモリの所有権を、複数の shared_ptr<T>スマートポインタと共有できる(普通のポインタとは共有できない)。
// このポインタは所有権を持つポインタの数を記録するカウンタを持っており、共通の所有権を持つ shared_ptr<T>がコピーされると内部でカウンタが増え、ディストラクタや明示的解放時に減る。全ての所有者がいなくなってカウンタがゼロになるとメモリが解放される。これにより最後の所有権が破棄された時の1度だけメモリが解放されて、deleteの重複が生じない。
// このポインタはコピーもムーブも可能。そして unique_ptrと同じく、明示的にムーブを定義しなくても、ポインタ変数だけは nullptrで初期化してくれる(ポインタ変数以外はゼロ初期化されない)。
// また、このスマポは内部でカウンタを利用しているため通常のポインタより処理が重く、配列の形に出来るが明示的に deleterを指定する必要がある。利用には #include <memory> が必要。
class SharedPtr {
public:
std::shared_ptr<int> sp_int1;
int int2 = 20;
SharedPtr() {};
SharedPtr(int a) : sp_int1(new int(a)) {}
};
int main() { // ブレークポイント設定して、ステップ実行しながら各変数とポインタが指すメモリを確認するとわかりやすいよ。
SharedPtr sharedPtr(10);
SharedPtr* sharedPtr2 = new SharedPtr(sharedPtr); // unique_ptrと違い、所有権を共有できるのでコピー可能
SharedPtr* sharedPtr3 = new SharedPtr();
*sharedPtr3 = std::move(sharedPtr); // unique_ptrを同じで、ムブコンを宣言していなくてもメンバの shared_ptr ポインタ変数はちゃんとムーブされて、ムーブ元は nullptrになる(普通のメンバ変数は放置)。
if (sharedPtr.sp_int1 == nullptr) { printf("ムーブ元 nullptrやん"); }
delete sharedPtr2; // インスタンスが破棄されて内部メンバのスマポも破棄されるが、まだ所有権を有するスマポがいるので自動解放されない。
delete sharedPtr3; // インスタンスが破棄されて内部メンバのスマポも破棄され、所有権を有するスマポが全て破棄されたので、所有権が共有されていたメモリは自動解放される。
sharedPtr2 = nullptr;
sharedPtr3 = nullptr;
}
// 使い方の注意点
int main() {
// shared_ptr ポインタは unique_ptrと同じく生成手段が限られる
std::shared_ptr<int> sp_int1(new int(10)); // コンストラクタの引数として、動的確保したメモリのアドレスを指定するか
std::shared_ptr<int> sp_int2;
sp_int2.reset(new int(10)); // 先に宣言だけしたものに、reset関数を使って後から代入するか
std::shared_ptr<int> sp_int3 = std::make_shared<int>(10); // make_shared関数を使うと、メモリとカウンタ同時に動的確保できて上二つよりも確保処理が軽くなる(こっちはC++11から使える)。ただし、deleterを明示的に指定する際には make_shared<T>(args...)は使えない。
// < >の引数は第一引数のみが型指定で第二引数は deleterの指定になるので、複数の引数を持つコンストラクタは std::pair<...>でまとめた型にする必要がある。
typedef std::pair<int, double> int_double_t;
std::shared_ptr<int_double_t> sp_multi = std::make_shared<int_double_t>(10, 0.4);
auto sp_multi = std::make_shared<std::pair<int, double>>(10, 0.4); // 上と同じ
// unique_ptrからのムーブも可能
std::unique_ptr<int> up_int1(new int(10));
std::shared_ptr<int> sp_int4(std::move(up_int1)); // ムブコン
std::unique_ptr<int> up_int2(new int(10));
std::shared_ptr<int> sp_int5;
sp_int5 = std::move(up_int2); // ムブ代入
// メモリ開放は shared_ptr.reset() で明示的に解放することも可能。ただし、所有権を持つポインタが他にもある時(全て破棄されていない時)は所有権を放棄する(nullptrが代入される)だけ。
sp_int1.reset();
// 所有者の確認
std::cout << "use_count=" << sp_int1.use_count() << std::endl; // use_count関数で所有者の数を確認出来るが、マルチスレッド環境下ではこの値は信頼出来なくなるので注意。
if (sp_int1.unique()) { // unique関数で所有者が唯一であることを確認出来る(C++20で削除された)
std::cout << "unique" << std::endl;
}
// アドレスを返したい時には、& ではなくget関数を使う。
int* pint;
pint = sp_int1.get(); // ポインタに代入すると、普通にアドレスを値コピーしたみたいに同じ参照先を指す(所有権を持つ)ポインタが、スマポの所有権管理に無関係に増えちゃうので注意!
// 所有権を放棄してアドレスを返す release()は用意されていない。
// 配列の形にもできる。
// ただ、operator[](size_t)は用意されておらず、代わりに get関数でアクセスする。また、deleterを明示的に指定する必要がある。
{
std::shared_ptr<int> ptrArray(new int[10], std::default_delete<int[]>()); // 第2引数で、配列用にdeleterを指定
for (int i = 0; i < 10; ++i) {
ptrArray[i]=i; // operator[]は使えないのでエラー
ptrArray.get()[i] = i; // 代わりに、get関数からアクセスできる
}
} // default_delete<int[]>を指定すれば、delete[]が呼ばれて解放される。
}
// std::shared_ptr<T> の指すメモリを参照する「 std::weak_ptr<T> 」
// weak_ptr<T> にshared_ptr<T>を格納すると、shared_ptr<T>の指すメモリへのアクセス権だけを得られる(所有権(解放する権利)はない)。つまり、shared_ptr<T>スマポ専用の参照変数的な感じ(ただし値ではなくアドレスの形)。
// 参照変数と同じで、アクセス中に参照先のメモリが所有権を持つものに解放される可能性はある。ただし、weak_ptrはアクセス中に shared_ptrの方で参照先メモリが解放されてもそれを検知出来る。
// コピーもムーブもできるが、weak_ptr同志でしかできない。また、デストラクタではアドレス先のメモリの解放は行われず、このスマポの参照が解除されるだけ。
// 所有権持つスマポを強参照、持たないこのスマポを弱参照と言って、strong refと weak refの表記でVSのデバッグ画面に数が表示される。
// weak_ptr<T>は shared_ptrが循環参照時に破棄されない問題を解決できるらしい(でもそもそも循環参照の設計にしたらダメじゃね?相互依存回避できない設計とかあるんか?)。
class SharedPtr {
public:
std::shared_ptr<int> sp_int1;
int int2 = 20;
SharedPtr() {};
SharedPtr(int a) : sp_int1(new int(a)) {}
};
int main() { // ブレークポイント設定して、ステップ実行しながら各変数とポインタが指すメモリを確認するとわかりやすいよ。
SharedPtr* sharedPtr = new SharedPtr(10);
int& ref_int = *sharedPtr->sp_int1; // 参照変数(所有権ないけど参照値へのアクセス権だけある)
printf("%d\n", ref_int);
std::weak_ptr<int> wp_int1(sharedPtr->sp_int1); // weak_ptrによる参照変数みたいな形(所有権ないけど参照値へのアクセス権だけある、ただしアドレスの形)
printf("%d\n", *wp_int1.lock());
delete sharedPtr; // インスタンスが破棄されて内部メンバのスマポも破棄され、所有権を有するスマポが全て破棄されたのでメモリは自動解放される。
sharedPtr = nullptr;
printf("%d\n", ref_int); // 参照変数は参照先が解放されてるかどうかが分からない。解放後なので不定値になっておかしくなってる。
// 参照先が解放されてるかどうかがアクセス時に分かる。nullptr だと解放されてる。
(wp_int1.lock() != nullptr) ? printf("%d\n", *wp_int1.lock()) : printf("参照先のメモリ解放されちゃってるよ"); // 単文で書くことでマルチスレッドでアクセス中に解放される心配が無くなる
// 複数の文で書くと、nullptr判定時は nullptrじゃなくても、処理の間のタイミングでマルチスレッドの他の場所で解放される可能性がある
if (wp_int1.lock() != nullptr) {
/* → nullptr判定時は nullptrじゃなくても、ここの部分みたいに次の処理との間のタイミングでマルチスレッドの他の場所で解放される可能性がある */
printf("%d\n", *wp_int1.lock());
}
else {
printf("参照先のメモリ解放されちゃってるよ");
}
}
// 使い方の注意点
int main() {
// 他スマポと同じく生成手段が限られており、また shared_ptr<T>の値しか格納できない
std::shared_ptr<int> sp_int1 = std::make_shared<int>(10);
std::weak_ptr<int> wp_int1(sp_int1); // コンストラクタの引数として、動的確保したメモリのアドレスを指定するか
std::weak_ptr<int> wp_int2;
wp_int2 = sp_int1; // 先に宣言だけしたものに、reset関数を使って後から代入する
std::weak_ptr<int> wp_int3(new int(10)); // ポインタを直接受け取ることはできないのでエラー
// ムーブやコピーもできるが、weak_ptr同志でしかできない
std::weak_ptr<int> wp_int4(wp_int1); // コピコン
std::weak_ptr<int> wp_int5;
wp_int5 = wp_int1; // コピ代入演算子
std::weak_ptr<int> wp_int6(std::move(wp_int1)); // ムブコン
std::weak_ptr<int> wp_int7;
wp_int7 = std::move(wp_int2); // ムブ代入演算子
// reset はアドレス先のメモリの解放ではなく、この参照の解除になる。所有権がないから。
wp_int4.reset(); // wp_int4が nullptrになるだけ
// 使用中に解放されるのを避けるため、そのままでは * や -> でアクセス出来ず、lock関数で shared_ptr<T>の右辺値(多分)を生成してアクセスする。
printf("%d", *wp_int5.lock()); // shared_ptrを生成
// 生成した shared_ptrは右辺値なので、このまま reset()してもメモリ解放もされないし、カウンタがデクリメントされることもない(共有するshared_ptrが1つ以上は必ず残っているので、このshared_ptrをnullptrにするだけ)。
wp_int5.lock().reset(); // 意味ない
// lock関数でアクセス時に、nullptrか否かで参照先のメモリが解放されてるかがわかる
if (wp_int5.lock() == nullptr) { printf("もうメモリ解放されてんで"); }
}
// 3つのスマポを使うタイミング
// ・全てのスマポは、「動的確保されたメモリ」を対象として、ポインタ変数や参照変数を使う時と同じタイミングで代替して使う。
// ・基本的に、所有権を持っているポインタは1インスタンスにつき1つだけのことが多く、そのようなときは unique_ptrを使う。
// ・たまに1インスタンスの所有権を複数のポインタで共有する設計をとることがあり、そのようなときには shared_ptrを使う。
// ・参照変数のように扱いたい時か、参照変数のように使いたいがアクセス中に参照先のメモリが所有権を持つものに解放される可能性がある時、そのようなときは shared_ptrと weak_ptrを使う。
// ・ムーブ系を自動で定義してほしい時(明示定義がめんどい or 明示定義忘れの予防)は、用途に応じて unique_ptr か shared_ptr を使う。
// ・ちなみに、shared_ptr は他二つと比べて重めなので、なるべく、所有権を共有する時には unique_ptrで代替表現したり(共有したものが利用/破棄される順番が固定なら、unique_ptrを順繰りにムーブすれば表現できる)、アクセス中にメモリが解放されることが明白なら weak_ptrを使って代替すると、処理が早くなる。
// 廃れたスマートポインタ「 std::auto_ptr<T> 」
// 実は C++11依然には std::auto_ptr<T> というスマートポインタもあったが、コピーすると自動でムーブされる(所有権が移り、コピー後にコピー元がアクセスできなくなる)という誤解を生みやすい仕様になっている。C++11以降では上記3つのスマポが完全に上位互換として存在するのでなるべく使うべきではない。
///ーー std::vector を使うときの注意点(コピコンとムブコン、スマポ) ーー///
// std::vector は、現在の要素数 size と容量 capacity がある。
// 要素を追加する時、追加された要素が左辺値の時は、capacityに空きがある状態で要素を追加すると、vectorの連続する新しいメモリの位置に追加された要素がコピコンされる。また、capacityに空きがない状態で要素を追加すると、追加された要素を含め全ての要素がコピコンされる(既存メモリの解放と再設置が生じる、、はずだけど、なぜかVCのデバッグではメモリ位置は変わらず、コピコンだけ生じる。コピコンだけは必ず行われるけど、実際に移動するのはメモリスペース足りない時だけなのかも)。これらの時にコピコンが削除されているとエラーになる。
// 要素を追加する時、追加された要素が右辺値の時は、capacityに空きがある状態で要素を追加すると、vectorの連続する新しいメモリの位置に追加された要素がムブコンされる(ムブコンが無いとコピコンされる)。また、capacityに空きがある状態で要素を追加すると、追加された要素だけがムブコンされて、残りの要素全ての要素はコピコンされる。左辺値の時と違い、これらの時にコピコンが無いとムブコンが、ムブコンがないとコピコンが代わりに使用され、両方ともない時だけエラーになる。
// つまり、std::vector を使うときは、追加される要素が左辺値の時はコピコンが、右辺値の時はコピコンかムブコンか両方かが使われる。そのため、std::vectorの要素や要素の内部メンバ変数にポインタを含んでいると、明示的にコピコンやムブコンを定義しないと、デフォルトのままだとアドレス(所有権)の共有が生じて、RAIIみたいにしてると deleteが重複してバグる。
// また、自動でムーブを定義してくれるスマポを使うとムブコン面は解決するが、追加する要素が左辺値の時はコピコンがあるので shared_ptrしか使えない。また、追加する要素が右辺値の時はコピコンがあるとコピコンも使いだすので、ムブコンしかない unique_ptrしか使えない。このようにスマポでも色々対処したり考えるべきことがあるので注意すべき(もしかして他の自動拡張のSTLコンテナ型も色々制約あったりしそう?てかいっそ自分で配列で再現したような新しいクラスを作った方が良いまであるかもな。足りなくなったら一定の大きさの配列動的確保して、全ての配列を合わせて2次元配列で管理するとか)。
// 要素追加が左辺値の時
struct Obj
{
Obj() { printf("コンスト\n"); }
~Obj() { printf("デスト\n"); }
Obj(const Obj& o) { printf("コピコン\n"); } // 全部コピコンになる
Obj(Obj&& o) { printf("ムブコン\n"); }
};
int main() {
Obj obj1, obj2, obj3;
std::vector<Obj> v1;
std::vector<Obj> v2;
// 予め capacityを確保してるしてるとき
v1.reserve(3);
printf("1\n");
v1.push_back(obj1);
printf("2\n");
v1.push_back(obj2);
printf("3\n");
v1.push_back(obj3);
// capacityが足りない時
printf("1\n");
v2.push_back(obj1);
printf("2\n");
v2.push_back(obj2);
printf("3\n");
v2.push_back(obj3);
printf("main終了\n");
}
// 要素追加が右辺値の時
struct Obj2
{
Obj2() { printf("コンスト\n"); }
~Obj2() { printf("デスト\n"); }
Obj2(const Obj2& o) { printf("コピコン\n"); } // 追加される要素以外はコピコンになる
Obj2(Obj2&& o) { printf("ムブコン\n"); } // 追加される要素はムブコンになる
// ムブコンとコピコン片方がないと、もう片方が代替する。コメントアウトしたりして確認してみて。
};
int main() {
std::vector<Obj2> v1;
std::vector<Obj2> v2;
// 予め capacityを確保してるしてるとき
v1.reserve(3);
printf("1\n");
v1.push_back(Obj2());
printf("2\n");
v1.push_back(Obj2());
printf("3\n");
v1.push_back(Obj2());
// capacityが足りない時
printf("1\n");
v2.push_back(Obj2());
printf("2\n");
v2.push_back(Obj2());
printf("3\n");
v2.push_back(Obj2());
printf("main終了\n");
}
// 上記のは push_backの例だけど emplace_backだと挙動がちょっと変わるので要確認かも。
// また、std::vectorは capacityを拡張する時の処理が重たい&上記の通り普通に追加する時と発動するコンストラクタ系の種類が異なるので、予め reserve()で要素数を確保しておくと、処理を軽くできるし、発動するコンストラクタ系の種類をある程度コントロールできる。
///ーー テスト ーー///
// 機能において、パラメータが取りうる値や発生しうる処理のパターンを予め固定し、それを複数の組み合わせに対して実行して不具合が生じないか確認すること。アプリやライブラリを作る際には必須。
// テストしたい範囲やパターンを予め決定して、.bat ファイルなどで PCに自動で実行&ログ取得させるようにすると、自動でテストを行うことができる。
// 特に、一つの関数や機能を対象にして行うことを単体テストといい、単体テストでは全体テストと異なりパターンが狭くて済むため、かかる時間も少なく網羅的に検査が可能になる。(全体になると機能の組み合わせが無数に生じてしまい、網羅的にテストするのは現実的でなくなる)
// ただし、テストする時にはテストの仕様を適切に考える必要がある。例えばパターンテストだと、生じうるパターンを正確に網羅的に把握してテストする必要がある。
// モック:テスト対象ではないが、テストする上で伴うテストには不要な処理をテスト用の簡易的なものに置き換えたもの。例えば、Twitterのユーザー情報を基に処理するある機能をテストしたい時に、毎回TwitterのAPIを叩いて実際のユーザーデータを持ってくるのは負荷重いし無駄なため、適当なユーザー名とパスワードを生成して返す代わりのモック関数を作るなど。
// 単体テスト用のフレームワークとして、 GoogleTest がすごい有名らしい
///ーー シングルトンパターン ーー///
// その型のオブジェクトがプログラム中に(ちゃんと機能するものが)ただ1つのみ存在することを保証し、かつそのオブジェクトに対する他のコードからアクセスが唯一である設計のこと。
// シングルトンはよく使われているが、使用するべきではない設計。なぜなら、グローバル関数/変数や staticメンバは、関数内部で利用する対象を指定しており引数では指定していないため、外部からのモックへの入れ替えによるテストなどを不可能にする。そして、機能を実装するに上ではテストは必要不可欠だし、シングルトンは内部に staticを必ず含んでしまうから。
// シングルトンの機能は、依存注入(DI)コンテナでオブジェクト数管理すれば解決できるし、DI 使えば疎結合にもなって万歳だから、DIコンテナを使うべき。(なので、ここではシングルトンの実装方法自体は省くよ)
class UserDataManager_SingleTon { // ユーザーデータを管理するシングルトンクラス(めんどいからコンストラクタ系は省略)
private:
int m_userId, m_userPassword;
static UserDataManager_SingleTon* instance;
public:
static UserDataManager_SingleTon& get_instance() { // 既にインスタンスがあればそれを返す、無ければ作って返す
if (!instance) instance = new UserDataManager_SingleTon;
return *instance;
}
static void destroy() { delete instance; instance = nullptr; }
int GetUserId() const { return m_userId; }
int GetUserPassword() const { return m_userPassword; }
void AccessUserData() {}; // ネットワークアクセスとかAPI利用とかを介してユーザーデータを取得する激重処理(ここでは実装は省略)。
};
class UserDataManager { // ユーザーデータを管理するシングルトンではないクラス
private:
int m_userId, m_userPassword;
public:
int GetUserId() const { return m_userId; }
int GetUserPassword() const { return m_userPassword; }
void AccessUserData() {}; // ネットワークアクセスとかAPI利用とかを介してユーザーIDを取得する激重処理(ここでは実装は省略)。
};
void UseUserDataFunction_UseSingleTon() { // ユーザーデータを使って何らかの処理する関数。内部でシングルトンクラスに依存している。
UserDataManager_SingleTon::get_instance().AccessUserData();
int userID = UserDataManager_SingleTon::get_instance().GetUserId();
int userPassword = UserDataManager_SingleTon::get_instance().GetUserPassword();
// 引数ではなく、関数内部で指定しているので、この関数の呼び出し側でモックオブジェクトに差し替えられない。
// → この関数 UseUserDataFunction_UseSingleTon() の処理をテストしたいのに、毎回 UserDataManager_SingleTon::get_instance().AccessUserData() の激重処理を強制させられる。
/* ~他の色々処理する機能~ */
}
void UseUserDataFunction_NoSingleTon(int userID, int userPassword) { // ユーザーデータを使って何らかの処理する関数。外部から指定できる引数に依存している
// 利用する対象を外部からの引数で指定しているので、この関数の呼び出し側で適当な int の数値にモックオブジェクトとして差し替えられる。
// → この関数の処理をテストしたい時に、激重処理を省ける。
/* ~他の色々処理する機能~ */
}
int main() {
UserDataManager u;
u.AccessUserData();
UseUserDataFunction_NoSingleTon(u.GetUserId(), u.GetUserPassword()); // 実際に使うとき。
int testUI = 1234; int testUP = 5678;
UseUserDataFunction_NoSingleTon(testUI, testUP); // テストするとき。適当な値にモックとして差し替えられる。AccessUserData()の激重処理を必要としない
}
///ーー 依存注入(DI) ーー///
//(主にインターフェースを用いて)部品の利用場所と部品の実装/指定場所を切り分けること。動的ポリモーフィズムしたり依存関係を逆転させたりテストを簡単にできる。
// テストに使う例として、上記の「シングルトン」にて引数を用意して外部からそこに当てはまる値を切り替えて処理を変えられるようにした。
int main() {
UserDataManager u;
u.AccessUserData();
UseUserDataFunction_NoSingleTon(u.GetUserId(), u.GetUserPassword()); // 実際に使うとき。
int testUI = 1234; int testUP = 5678;
UseUserDataFunction_NoSingleTon(testUI, testUP); // テストするとき。適当な値にモックとして差し替えられる。AccessUserData()の激重処理を必要としない。(前回)
}
// しかし、いちいち引数を変えたり、その前の処理の有無を変えたりするのはコード嵩増しするし再利用性に欠ける、、。 → インターフェースを使えば解決できるぞ!
class IUserDataManager { // ユーザーデータ管理管理クラス用のインターフェース
public:
virtual int GetUserId() const = 0;
virtual int GetUserPassword() const = 0;
virtual void AccessUserData() = 0;
};
class UserDataManager : public IUserDataManager {
private:
int m_userId, m_userPassword;
public:
int GetUserId() const { return m_userId; }
int GetUserPassword() const { return m_userPassword; }
void AccessUserData() {}; // ネットワークアクセスとかAPI利用とかを介してユーザーIDを取得する激重処理(ここでは実装は省略)。
};
class Mock : public IUserDataManager { // テスト様のモッククラス
private:
int m_userId, m_userPassword;
public:
int GetUserId() const { return m_userId; }
int GetUserPassword() const { return m_userPassword; }
void AccessUserData() { m_userId = 1234; m_userPassword = 5678; }; // 仮の値を入れるテスト
};
void UseUserDataFunction_NoSingleTon(IUserDataManager& u) { // ユーザーデータを使って何らかの処理する関数。
int userID = u.GetUserId();
int userPassword = u.GetUserPassword();
/* ~色々処理する機能~ */
}
enum ENVIRONMENT {
RELEASE,
TEST
};
int main() {
IUserDataManager* u = nullptr;
// 部品の実装・指定部分。ここでは利用部分と同じ所で記述しているが、本当は他の関数とかクラスとかの別の場所に移す。
{
int target;
/* ~開発環境決める処理~ */
switch (target)
{
case RELEASE:
u = new UserDataManager();
break;
case TEST:
u = new Mock();
break;
/* ~他の開発環境にも続いていく~ */
}
}
// 部品の利用部分。動的ポリモーフィズムで、部品の種類に依存せず同じ記述で実行できる。
{
u->AccessUserData();
UseUserDataFunction_NoSingleTon(*u);
/* ~他にも色んな機能の利用処理~ */
}
// つまりインターフェースを使うと、部品の「実装・指定部分」と「利用部分」を分けられるので、変更や差し替えを行うときには「利用部分」には触れずに「実装・指定部分」をカスタマイズすれば良い、という風に機能分離ができて、機能の再利用や拡張や管理が容易になる。
// また同じくポリモーフィズムを実現できるものにジェネリックがあるが、ジェネリックは普通の関数と同じく部品の「実装・指定部分」と「利用部分」で分けられないので依存注入はできない。
}
///ーー DIコンテナ ーー///
// DIコンテナは DIのお助けフレームワーク。
// DIの欠点は、使用側と実装側を分けて、使用側の外で部品の実装・指定を行い、それを引数として外部から使用側に当てはめるため、広い範囲で用いる関数の場合、引数の数がどんどん増えていく。
class IPartsA { /*~なんかの機能部品Aのインターフェース~*/ };
class IPartsB { /*~なんかの機能部品Bのインターフェース~*/ };
class PartsA_1 : public IPartsA { /*~なんかの機能部品Aを踏襲したクラスその1~*/ };
class PartsA_2 : public IPartsA { /*~なんかの機能部品Aを踏襲したクラスその2~*/ };
class PartsB_1 : public IPartsB { /*~なんかの機能部品Cを踏襲したクラスその1~*/ };
class PartsB_2 : public IPartsB { /*~なんかの機能部品Cを踏襲したクラスその2~*/ };
class NoDiContainer {
private:
IPartsA* m_partsA;
IPartsB* m_partsB;
public:
NoDiContainer(IPartsA* partsA, IPartsB* partsB) :m_partsA(partsA), m_partsB(partsB) {} // 例えばコンストラクタで指定してる。
};
int main() {
NoDiContainer ndc(new PartsA_2, new PartsB_1);
}
// ここから、「あっ、この NoDiCOntainer クラスに機能部品Cの機能を追加したいぞ!」となったとき、当然 NoDiContainerの実装の中身も変更必要だし、それ以上に、既に広く使われているであろう NoDiContainer コンストラクタの引数を、使用されている場所全てで修正する必要があり、非常に追加修正に弱い設計になる。この部分を省力化するライブラリが DI コンテナ。
// C++ の DIコンテナライブラリとしては Hypodermic とか?があるらしいので、調べてつかってみてね。 https://www.niwaka-plus.com/entry/Hypodermic_how_to_use
// あとは、自作する方法もあるらしい。https://qiita.com/sadnessOjisan/items/d05e35e34d2d1fb7e844
///ーー テンプレート(ジェネリック) ーー///
// テンプレート(ジェネリック)は 関数やクラスを、引数や返り値やメンバの型に依存しないように汎用性高くしたもの(静的ポリモーフィズム)。
// 注意すべきは、テンプレートを使う時は、よくある「ヘッダ⓪に宣言、ソース①に実装、別のソース②で使用」の形でファイルを分けるとエラーが生じる。
// なぜなら、テンプレートの型は、ソース②の使用時に決まるが、使用を担うソース②も実装を担うソース①も include でヘッダ⓪しか参照しないので、実装を担うソース①視点だとソース②が見えなくて型が分からないため。
// これを解決するために、インクルードされる可能性のあるファイルでテンプレートを使う時は、「ヘッダ内に実装を全て書く(か特定の型に対するインスタンスを明示的に書く)」ことが必要。(特定の型に対するインスタンスを明示的に書く場合は、使われる各型を全て実装するソース①側に記さないといけないので読みにくいし毎回書く時点でポリモーフィズムの意味があんま無くなる)
// テンプレート関数:引数と返り値を汎用的に。
template <typename T, typename U> T add(T x, U y) { // template <typename 引数or返り値で使う型名...> 普通の関数宣言〜。 typename 部分は class でも可
return x - y;
}
// テンプレート関数の特殊化:特定の型の処理を例外として指定できる。
template <> string add(string x, string y) { // template <> 普通の関数宣言~
return x + y; // float 型の時だけ引き算をする処理に。
}
template <> double add(int x, int y) { // エラー。あくまでも返り値と引数の型は汎用型を踏襲する必要がある(返,1引,2引 = T,T,U)。つまりここでは、返り値と第一引数の型は同じである必要がある
return x * y;
}
int main() {
// 型に依存せず加算処理
cout << add<int, float>(4, 3.0f) << endl;
cout << add<float, float>(5.0f, 4.5f) << endl;
// string 型のみ加算処理
cout << add<string, string>("ABC", "DEF") << endl;
cout << add(30, 2) << endl; // 型指定は省略できるけど、分かりにくいし誤読するので記載するべき。
}
// テンプレートクラス:クラス内のメンバ変数、メンバ関数を汎用的に。
//---------------calc.hpp
template<typename T, typename U> class CCalc { // template<typename 内部で使う型名...> 普通のクラス宣言〜
private:
T m_n1;
U m_n2;
public:
// テンプレクラスなのでヘッダに実装も記載。
void set(const T n1, const U n2) { // テンプレ関数は、多重定義が許可されているので inlineキーワードが要らない。
m_n1 = n1;
m_n2 = n2;
};
T add() const {
return m_n1 + m_n2;
}
};
//---------------main.cpp
int main() {
CCalc<int, float> i1;
CCalc<string, string> i2;
i1.set(1, 2);
i2.set("ABC", "DEF");
cout << i1.add() << endl << i2.add() << endl;
return 0;
}
//---------------
// concept による制約。
// デフォルトのテンプレート設計だと全ての型を当てはめられるので、入れたらバグるような型も入れれてしまう。一応特殊化したら特定の型に対する対応は出来るけど、網羅的に全ての型に対して行うのは現実的ではない。C++20 以降では、concepts キーワードでテンプレートの型の範囲を制限できる。
// requires {} 式で制約を列挙でき、その式を concept 型の変数に代入できる。
template<typename T> concept C1 = requires {
typename T::type; // 型 T がメンバ型に type を持つという制約
};
// 複数ある時は、全ての条件に当てはまることが制約になる
template<typename T> concept C2 = requires (T a, int b) { // concept の typename で指定する型は常に1つで、requires の引数は複数持てるが常に typenameと同じ第1引数の型に対する制約になる
a.push_back(b); // "型 T がメンバ関数に intを引数にする push_back()という関数を持つ" という制約(第二引数に対する制約はない)
{ a[b] } -> C1<int>; // "型 T に operator[] があり、C1<decltype(a[b]), int> == true" という制約
std::size(a); // "非メンバ関数 std::size が引数に型 T のオブジェクトを受け取れる" という制約
/* ~他にも色んな制約あるけど、書き方特徴的だから調べて~ */
};
// 制約が1つの時は requires 文がなくてもそのまま書ける
template<typename T, typename U> concept C3 = std::is_convertible_v<T, U>;
// 使うときは、typename 部分を定義した concept に入れ替える
template <integral_c<int> T, integral_c<int> U> T add(T x, U y) {
return x + y;
}
//---------------使用例.cpp
template<typename T> concept integral_c = requires {
std::is_integral_v<T>;
};
template <integral_c T, integral_c U> T add(T x, U y) { // concept で制限
return x - y;
}
template <> string add(string x, string y) { // 特殊化も一緒に使える
return x + y;
}
int main() {
cout << add<int, float>(4, 3.0f) << endl;
cout << add<float, float>(5.0f, 4.5f) << endl;
cout << add<string, string>("ABC", "DEF") << endl;
}
//---------------
// ポリモーフィズム(同名で同形式なのに異なる機能を持つことができること)を可能にする設計としては、実行時に処理が決定する 継承と virtual関数(動的)か、コンパイル時に処理が決定する テンプレートと特殊化(静的)かのどちらかになることが多い。
// ほとんどの場合どちらか片方でしか実現できないポリモーフィズムはなく、機能的には互換可能な設計になるが、どちらの設計にするかで処理速度と型の柔軟性&可読性のトレードオフが生じる。
// 前者は、関数呼び出しに VTableを参照する処理を挟む、インライン展開による高速化不可、スタック領域の圧迫などの点でパフォーマンス部分にデメリットがある。後者はこれらのデメリットが無い上、実行時の処理をビルド時に行えるので処理を高速に行えるが、コンパイルに時間がかかる、依存注入ができなかったり記述が煩雑になる点で型の柔軟性が virtual関数より低下する、デバッグがしづらいなどの部分にデメリットがある。
//---------------動的ポリモ(virtual関数と継承)
// 機能の「実装部分」
struct IICharacter { // 最下層ベースになるキャラクターインターフェース
virtual ~IICharacter() noexcept = default;
};
struct INPC : IICharacter { // NPCインタ
virtual void speak() const = 0;
~INPC() noexcept override = default;
};
struct IEnemy : IICharacter { // Enemyインタ
virtual void attack() const = 0;
~IEnemy() noexcept override = default;
};
struct Villager : INPC { // 村人NPC
void speak() const override { puts("会話:アドバイスする"); }
~Villager() noexcept override = default;
};
struct Slime : IEnemy { // スライムEnemy
void attack() const override { puts("アタック:体当たりする"); }
~Slime() noexcept override = default;
};
struct Merchant : INPC, IEnemy { // 商人NPC(Enemyにもなりうる)
void speak() const override { puts("会話:アイテムを売買する"); }
void attack() const override { puts("アタック:護身用の銃で撃つ"); }
~Merchant() noexcept override = default;
};
void NPCSpeak_nTimes(const INPC& npc, int n) {
for (int i = 0; i < n; i++) { npc.speak(); }
}
void EnemyAttack_nTimes(const IEnemy& enemy, int n) {
for (int i = 0; i < n; i++) { enemy.attack(); }
}
enum class NPC_TYPE {
VILLAGER,
MERCHANT
};
enum class ENEMY_TYPE {
SLIME,
MERCHANT
};
int main() {
// 機能の「指定部分」
/* ~何らかの方法でどの子クラスにするか選択~ */
INPC* npcs[2];
int npcTarget[2] = { 0,1 };
for (int i = 0; i < 2; i++) {
switch (npcTarget[i])
{
case int(NPC_TYPE::VILLAGER) :
npcs[i] = new Villager();
break;
case int(NPC_TYPE::MERCHANT) :
npcs[i] = new Merchant();
break;
}
}
// 機能の「利用部分」
// 上記の「実装・指定部分」が変更しても、こちらは変更する必要が全くない。(動的ポリモのみ依存注入が可能)
for (INPC* npc : npcs) NPCSpeak_nTimes(*npc, 2);
}
//---------------静的ポリモ(conceptと関数テンプレート)
template<typename T> concept ConceptCharacter = true; // 最下層ベースになるキャラクターコンセプト
template<typename T> concept ConceptNPC = ConceptCharacter<T> && requires(T npc) { // NPCコンセプト
npc.speak();
};
template<typename T> concept ConceptEnemy = ConceptCharacter<T> && requires(T enemy) { // Enemyコンセプト
enemy.attack();
};
struct Villager { // 村人NPC
void speak() const { puts("会話:アドバイスする"); }
};
struct Slime { // スライムEnemy
void attack() const { puts("アタック:体当たりする"); }
};
struct Merchant { // 商人NPC(Enemyにもなりうる)インターフェースの多重継承も必要ない。
void speak() const { puts("会話:アイテムを売買する"); }
void attack() const { puts("アタック:護身用の銃で撃つ"); }
};
template<ConceptNPC T> void NPCSpeak_nTimes(const T& npc, int n) {
for (int i = 0; i < n; i++) npc.speak();
}
template<ConceptEnemy T> void EnemySpeak_nTimes(const T& enemy, int n) {
for (int i = 0; i < n; i++) enemy.attack();
}
enum class NPC_TYPE {
VILLAGER,
MERCHANT
};
enum class ENEMY_TYPE {
SLIME,
MERCHANT
};
int main() {
// 機能の「指定部分」と「利用部分」普通の関数と同じく機能の「指定部分」と「利用部分」が分離できないので依存注入はできない。
/* ~何らかの方法でどの子クラスにするか選択~ */
int npcTarget[2] = { 0,1 };
for (int i = 0; i < 2; i++) {
switch (npcTarget[i])
{
case int(NPC_TYPE::VILLAGER) :
Villager v;
NPCSpeak_nTimes(v, 2);
break;
case int(NPC_TYPE::MERCHANT) :
Merchant m;
NPCSpeak_nTimes(m, 2);
break;
}
}
}
// まとめ
// | 記述 | 特徴 |
// 動的ポリモ | クラスを継承定義 & 関数は普通に定義 → 機能の指定 → 機能の利用 | 依存注入可能、多重継承頻出、ランタイム処理重い |
// 静的ポリモ | クラスを普通に定義 & 関数はテンプレート定義 → 機能の指定と利用 | 依存注入不可、多重継承皆無、ランタイム処理軽い |
///ーー Observerパターン ーー///
// 一対多数 のクラス間でデータのやり取りをするときに効果を発揮する設計で、「一つのクラスから登録した複数のクラスに対して、任意のタイミングで一方通行的に同時にデータを渡す」というもの。
// 特定の状態の変化からそれが複数の対象に影響を及ぼすときに非常に良く使える。 GUI、データ監視、イベント駆動システム、リアルタイム通知、ゲームの状態管理などなど、、。
// データを渡す元を Subject/Observable といい、渡す先を Observerという。またデータを渡す元と渡す先を関連付けることを Subscribe/登録する という。
// 「時刻の変化」に伴い、コンソール画面に「更新されたことを出力」と「時刻を出力」を行う IObserverパターン
class IObserver;
class ISubject;
class IObserver { // Observer基底インタ
public:
virtual void Update(ISubject&) = 0;
};
class ISubject { // Subject基底インタ
private:
std::vector<IObserver*> m_obsList; // 登録した Observer一覧
public:
void Attach(IObserver* obs) { // 登録
m_obsList.emplace_back(obs);
}
void Detach(IObserver* obs) { // 登録解除
m_obsList.erase(std::remove(m_obsList.begin(), m_obsList.end(), obs));
}
void Notify() { // 通知
for (const auto& i : m_obsList) {
i->Update(*this);
}
}
};
class ClockSecond_Subj : public ISubject { // 秒数の状態を持つ Subject
private:
unsigned long m_second = 0;
public:
void Tick() { // 秒数が進む
m_second++;
ISubject::Notify();
}
unsigned long const GetSecond() { // アクセッサー
return this->m_second;
}
};
class DisplayUpdate_Obs : public IObserver { // 秒数の更新発生を出力する Observer
private:
ClockSecond_Subj* m_subjClock = nullptr;
public:
DisplayUpdate_Obs(ClockSecond_Subj* sub) : m_subjClock(sub) {
m_subjClock->Attach(this);
}
public:
void Update(ISubject& sbj) override {
if (this->m_subjClock == &sbj) {
this->Dislay();
}
}
void Dislay() {
std::cout << "ClockSecond_Subj is Updated." << std::endl;
}
};
class DisplaySecond_Obs : public IObserver { // 更新された秒数を出力する Observer
private:
ClockSecond_Subj* m_subjClock = nullptr;
public:
DisplaySecond_Obs(ClockSecond_Subj* sub) : m_subjClock(sub) {
m_subjClock->Attach(this);
}
public:
void Update(ISubject& sbj) override {
if (this->m_subjClock == &sbj) {
this->Dislay();
}
}
void Dislay() {
std::cout << "Updated: " << this->m_subjClock->GetSecond() << " (sec)" << std::endl;
}
};
int main(int argc, char const* argv[]) {
ClockSecond_Subj clock; // Subjectインスタンス生成
DisplayUpdate_Obs disp_update(&clock); // Observerインスタンス生成&登録
DisplaySecond_Obs disp_sec(&clock); // Observerインスタンス生成&登録
clock.Tick(); // Subject の状態が変化 → Observerが更新
clock.Tick(); // IObserverは登録した順に実行される。DisplayUpdate_Obs → DisplaySecond_Obs の更新の順番
clock.Tick();
}
///ーー 例外処理(C++) ーー///
// プログラムの継続を阻止する例外(エラー)が生じた場合は、その通知と、例外に対する対処が必要とされる。
// 例外が生じたかの判定は、関数の返り値に boolで処理が成功したか否かを逐一返すか、例外の送出を行うことでできる。C++ では例外の送出が可能で C では無理。
// C++ では例外が送出されるとユーザーが作った例外ハンドラ(catch)に処理が移り、それが存在しない場合は、プログラムはコンパイラのデフォルト機構によって強制終了する。
// 例外処理は制御フローをその例外に対処する例外ハンドラの箇所に移せるが、例外を送出する処理(throw)は重いので、例外処理時にだけ使い通常の条件分岐時などには使うべきではない。
// また、例外送出が生じると catchに処理フローが移るが、その際には元々の関数を抜けるので、元々の関数内部のローカル変数たちはその時点で破壊されることに注意(スタック領域上のクラスはデストラクタもちゃんと呼ばれる。普通に関数のスコープ終了と同じ感じになる)。またこのため、例外時の情報は「例外として送出するクラス」の中に全て記録する。例外時の各情報をポインタなどで記録すると、ポインタの先が関数内のローカル変数だった場合に catchに移動された時に失われるから。そして、リカバリーの際にも、ローカルで確保されたメモリを例外時の情報から解放しないと、ローカルのポインタは消えてるので、確保されたメモリへのアクセスできずにメモリリークする。(RAII設計で作れば catch中に解放処理を省ける?ただし、デストラクタでエラーを検出するような設計をしてはいけない?)
// →これでもどこまで破棄されるんだ?強制終了じゃなくてリカバリーしようとしたら途中から戻れたりするのか?ためそうか。そして、もう一つどのくらいまで破棄されるのか&メモリリークしないようにするリカバリー込みの例が欲しいかも。
// → catch {} 中のreturn と、throw でどこまで戻るのか、保持されてた変数たちはどうなのるか知りたい。
// C++ の例外の送出に関する仕組みには throw(送出), try(検知), catch(捕捉と対処)の3つのキーワードがある。
// throw 値、で例外を明示的に送出する。値は組み込み型、クラス型、enum型等なんでも良いが、大抵クラス型の値を投げる(使い捨てプログラムの場合は適当に int型で 1234とかを投げてもOK)。この時の throw で送出される値は、一時オブジェクトとして catchブロックの最後まで有効になる。ユーザー指定の throw で明示的に例外を送出しなくても、特定のエラー状況では標準で用意された例外が送出される(std::bad_alloc()とか)
// try{} ブロック内に、例外が発生する可能性がある処理を入れる。後の catch処理ではこの中で送出された例外しか扱えない。各 try{} ブロックには、直後に対応する catch(){} ブロックが連続して 1 つ以上存在する。
// catch(){} は()内で捕捉したい例外を、送出した例外の判別をするだけなら型名のみ、送出した例外を catch内で引数として使いたいなら型名+引数の形で指定し {}内で例外が生じた際の処理を記す。()内を型名+引数の形にして送出した例外を引数に渡すときは、throw で送出される値は一時オブジェクトとして catchブロックの最後まで有効なので、この時にはコピー渡しではなくポインタ渡しや参照渡しもを使うべき。try{} の後に連続する全ての catchブロックの()内の型は異なっている必要があり、一つでも同じのがあるとバグる(コンパイラは通るときあるから注意!)。try{} 内で送出された例外の値は、プログラムの処理順上で最も近い場所にある catch(){} の()に渡されて、プログラムのフローは例外の送出場所から catch(){} の{}内に移る。またcatch(){} 内でも、throw; と記すと現在の例外の型と値を再送出して、より上位のtry{}の中で入れ子状にできる。
//---------------
class IntArray { // 整数配列のクラス
public:
// 例外を扱いたい時はエラークラス使うと便利。IntArrayクラス内でのみ使うことを想定しているから内部クラスに。
class Error_MinusAccess {}; // 添え字が範囲外に小さかった時のエラークラス
class Error_OverFlowAccess { // 添え字が範囲外に大きかった時のエラークラス
private:
const IntArray* errorObj; // 例外を送出した IntArrayオブジェクトへのポインタ
int index; // 例外を送出した際の添え字の値
public:
Error_OverFlowAccess(const IntArray* p, int i) : errorObj(p), index(i) { } // コンスト
int get_Index() const { return index; } // アクセッサー
};
private:
int m_size;
int* pFirst;
public:
explicit IntArray(int size) : m_size(size) { pFirst = new int[m_size]; } // コンスト
~IntArray() { delete[] pFirst; }
IntArray(const IntArray& x) { // コピコン
if (&x == this) { // 自分自身の時の回避
m_size = 0;
pFirst = nullptr;
}
else {
m_size = x.m_size;
pFirst = new int[m_size]; // アドレスだけコピーではなくディープコピーを
for (int i = 0; i < m_size; i++) {
pFirst[i] = x.pFirst[i];
}
}
}
// = のオーバーロード。
IntArray& operator = (const IntArray& x) {
if (&x == this) { return *this; } // 自分 = 自分 の時の回避
m_size = x.m_size;
delete[] pFirst;
pFirst = new int[m_size];
for (int i = 0; i < m_size; i++) {
pFirst[i] = x.pFirst[i];
}
return *this;
}
// [] のオーバーロード。配列と同じように インスタンス名[] で要素にアクセスできるし、サイズが範囲外だと指定の例外投げる
int& operator [] (int i) {
if (i < 0) { throw Error_MinusAccess(); }
if (i >= m_size) { throw Error_OverFlowAccess(this, i); }
return pFirst[i];
}
// インスタンスが constの時の [] のオーバーロード。
const int& operator [] (int i) const {
if (i < 0) { throw Error_MinusAccess(); }
if (i >= m_size) { throw Error_OverFlowAccess(this, i); }
return pFirst[i];
}
public:
int get_Size() const { return m_size; }
};
//---------------
void func(int size, int num) {
try
{
IntArray x(size);
for (int i = 0; i < num; i++) {
x[i] = i;
}
const IntArray y(size);
for (int i = 0; i < num; i++) {
printf("x[%d]=%d ", i, x[i]);
printf("y[%d]=%d ", i, y[i]);
}
}
catch (IntArray::Error_MinusAccess) { // Error_MinusAccess型を捕捉。()では、送出した例外を内部で使用しないので例外の種類を型名のみで指定。
printf("添え字がマイナスになってるよ");
return; // このreturnでどこまで遡って返るんやろうか、、。
}
catch (IntArray::Error_OverFlowAccess& err) {// Error_OverFlowAccess型を捕捉。()では、送出した例外を内部で使用するので型名+引数で指定。一時オブジェクトだけど参照渡しできる(なぜか右辺値参照じゃない)。
printf("添え字がオーバーフローしてるよ。添え字[%d]", err.get_Index());
return;
}
catch (...) { // catch(...)では未捕捉の全ての例外を捕捉できるが、型も仮引数も書くことができないので、どんな例外が発生したのかブロック内では一切分からない。
printf("添え字は適切で、それ以外のエラーが生じてるよ\0");
throw; // throw; で例外を再送出できる。より上位のtry{}の中で入れ子に。
}
}
int main() {
int size, num;
cout << "要素数:";
cin >> size;
cout << "データ数:";
cin >> num;
try {
func(size, num);
}
catch (bad_alloc) { // bad_alloc型を捕捉
printf("メモリの確保に失敗しました");
abort(); // 強制終了
}
catch (...) {
printf("添え時不適切でも、メモリ確保失敗でもない、エラー生じてるよ");
abort(); // 強制終了
}
printf("main関数正常に終了");
}
//---------------
// noexceptは関数が例外を投げないことを保証できる。noexceptはテンプレートやラムダ、メンバ関数などにも使用できる。
// これによってコンパイラは例外処理に関連する追加コードの生成が必要なくなりパフォーマンスが向上する可能性がある。また、関数が例外を投げる可能性がないことを他の開発者に明示的に示すことができ、コードの可読性と安全性が向上する。基本的には、例外を投げないと断定できる安全な処理は、noexcept を使用してパフォーマンスの向上を図るべき。
// しかし、noexcept 関数内で例外が発生した場合プログラムは普通に終了するため、使う場面は、「関数内で例外を投げるコードがない場合」「例外が生じても内部でキャッチ、処理し、外部に伝播させない場合」「関数が基本的な操作のみを行い、例外が発生する可能性が非常に低い場合」といった場合に限られ、逆に「関数内で外部リソースへのアクセスや動的メモリ割り当てなど、例外が発生しやすい操作を行う場合」「外部ライブラリやAPIを呼び出し、その結果が不確定な場合」などの場面では使用するべきではない。
void safeFunction() noexcept {
// 例外を投げない安全な処理
// ...
}
void warningFucntion() {
// 例外を投げる可能性のある処理
// 外部APIつかうとか、メモリ確保/解放系の処理するとか
}
// また、式が例外を投げるかどうかを確認するために使用できる noexcept演算子や、関数が特定の条件下でのみ例外を投げないことを保証する 条件付きnoexcept もあり、テンプレート関数や汎用的な関数で特に有用に働くらしい(そんな重要そうじゃなかったから気になったら調べて)。
// new によるメモリ確保について、例外はそれをキャッチする記述があるところまで関数の呼び出し元へ遡って処理されるので(どゆこと?)対処方法が共通である場合は毎回その場に try/catch を書く必要はない。 特にメモリの確保を失敗する状況では出来ることがほとんどない (メッセージを出して終了するくらい) ので main にひとつ書けば充分になることも多い。ただ呼び出し元へ遡る過程で自動変数は解体されるが、デストラクタ以外の形でリソースの後始末をしているとその処理を通過しない? https://zenn.dev/melos/articles/9e21dc8bd562b2#comment-7ffc91a18efc9d
///ーー プロセスとスレッド(非同期処理の前の前提知識) ーー///
// アプリとプロセスとメモリ
// アプリケーションに必要となる.exeとか画像とかのデータは全てストレージ上(HDD/SSD等)に保存されており、それを OS がメモリ(メモリモジュール)に持ってきて配置してアプリは実行される。
// Windows OS において、プログラムの実行単位はプロセス(process)といい、アプリが起動されると1つ以上のプロセスが起動する(Windowsのタスクマネージャーから確認できる)。
// メモリ領域はプロセスごとに区切られて管理されており、それらプロセスは独立して領域を有している。つまり、アプリAが起動した時にプロセスaとbが起動された場合、この2つが同じデータを使っていたとしてもメモリ領域は共有されない。既に先述したスタックやヒープ等のメモリ領域も、これらプロセスごとに独立して管理される。
// メモリ上に使われていないプロセスのデータがあったり、メモリが足りない場合は逐次省かれ削除されていき、現在重要なプロセスが多くのメモリを消費するようにメモリは(多分OSによって)自動で整地されていく。どのくらい重要視するかはプロセスの優先度によって決まる。(また環境によってはどんなに重要でも1プロセスに対しての上限が決まっていることもある Win 32bitなら1プロセス最大2GB、64bitならアプリ側が制限しない限り無制限、、、など)。
// アプリA アプリZ
// ↓→→→→→→ ↓
// ↓ ↓ ↓
// ________________________メモリ__________________________
// | | | | |
// | プロセスa | プロセスb | プロセスz | |
// |_____________|_____________|_____________|______________|
//
// ↑ ↑ ↑
// この3つのメモリ領域は独立している
// CPUとプロセス
// メモリに配置されたプロセスのデータを CPU があれこれ操作することで、プロセスは実行されていく。
// CPUにはコアというものが1つ以上あり、またコアの中にはスレッドといものが1つ以上存在する(昔は1コア1スレッドだったけど、今は1コア2スレッドが主流)。
// CPUスレッド1つにつき同時に1つのみのプロセスを実行できる(1スレッドが対処できるプロセスが1つなだけで、プロセス側は複数のCPUスレッドを使える)。
// 複数のプロセスが同時進行的に1つのCPUスレッド上で実行されるように見えることがあるが、これは実際には同時進行ではなく、高速で複数のプロセスを切り替えながら同時には1つのみのプロセスを行っているだけ(並行処理)。この切り替え(コンテキストスイッチ)は超高速なので、1つのCPUスレッドしか使っていなくても、複数のプロセスを実行、例えば Youtubeを見ながら Twitchを見てたとしても、同時進行的に見せることができる。
// 並行処理を行うと別プロセスを同時進行的に見せることが出来るが、立ち上げているプロセスの数や処理負荷が大きくなってくると1つのCPUスレッド上で高速にプロセスを切り替えるのもに限界がくる。そこでCPUコアやCPUスレッドを複数用意することで、独立したプロセスたちを多く実行していても負荷に耐えるようにしたのがマルチコア・マルチスレッディング。つまりCPUのコア数やスレッド数が多いものが評価されてるのは、プロセスを一度により多く実行できるから。
// また、複数のプロセスを完全に同時進行で実行する(並列処理)には、まずハード側でマルチコア・マルチスレッディングのCPUが必要になる。ハード側にマルチコア・マルチスレッディングのCPUが無いと、或いは用意されている数が少ないと、並列処理のプログラミングを構築してもスレッドが足りない分は並列処理は行われない。
// ________________________メモリ_________________________
// | | | | |
// | プロセスa | プロセスb | プロセスz | |
// |_____________|_____________|_____________|_____________|
//
// ↓ ↓ ↓
// ↓→→→→ ↓ ←←←←↓→→→→
// ↓ ↓ ↓ ↓ ↓ ↓
//
// _________________CPU(4コア8スレッド)_________________________
// | コア | コア | コア | コア |
// | a担 | a担 | b担 | z担 | z担 | z担 | | |
// |_______|_______|_______|_______|_______|_______|_______|_______|
//
// ↑プロセスは複数のCPUスレッドを使える一方、1つのCPUスレッドは同時に1つのみプロセスを担う↑
// プロセスとスレッド
// プロセス内の実行単位であるプログラムの1連の流れのことをスレッドという(先述のCPUスレッドの方ではなくプログラムの方。アプリ>プロセス>スレッド)。1プロセス内に1スレッドしかないものをシングルスレッド、複数スレッドあるものをマルチスレッドという。
// 各スレッドは同時に1つのCPUスレッドしか使えない。プロセスの扱いと同じく1つのCPUスレッドで複数のマルチスレッドが処理されているように見える時は、コンテキストスイッチで高速に切り替えながら1つのスレッドだけが並行処理されている。実は、先述したプロセスは複数のCPUスレッドを使えるというのは、プロセス内にあるそれぞれのスレッドが、個別の1CPUスレッドを使うことによって実現されていた。
// スレッドはプロセスと違い、同じプロセス内だとスレッド同士で静的領域やヒープ領域などの記憶領域を共有する(スタックとレジスタだけスレッド間でも独立する)。そのため、マルチスレッドの場合、同じリソースにアクセスる時は競合に気を付ける必要がある(コンフリクトとスレッドセーフ)。
// ________________________メモリ_____________________________
// | | | | |
// | プロセスa_ | プロセスb_ | プロセスz_ | |
// | | | | | | | | | | |
// | |スレッドa1| | |スレッドb1| | |スレッドz1| | |
// | |スレッドa2| | | | | |スレッドz2| | |
// | | | | | | | |スレッドz3| | |
// | |__________| | |__________| | |__________| | |
// |______________|______________|______________|______________|
//
// ↓ ↓ ↓
// ↓→→→→ ↓ ↓←←→↓→→→→↓
// ↓ ↓ ↓ ↓ ↓ ↓
//
// _________________CPU(4コア8スレッド)_______________________________
// | コア | コア | コア | コア |
// | a1担 | a2担 | b1担 | z1担 | z2担 | z3担 | | |
// |________|________|________|________|________|________|_______|_______|
//
// ↑同時的には各スレッドは1つのCPUスレッドと一対一の関係になっている↑
// 非同期処理
// 非同期処理とは、ある処理が終了するのを待たずに別の処理を実行すること。よく混同されがちだが、並列処理や並行処理のことを指しているわけではない(実現する上でどちらかの形になる)。
// そのため、色々なプログラム言語でよく非同期処理として関数名が挙がる async~ とか thread~ とかの関数も、ちゃんと内容を見ないと、実際は1コア1スレッドだけを使う並行処理だったり(関数単位の並行処理?)、マルチスレッドな並行処理だったり、並列処理だったりする(JavaScriptとかPythonとかが確かそう https://qiita.com/Toyo_m/items/992b0dcf765ad3082d0b)。
// とりあえず、C++ では後述する std::thread、std::async は複数のCPUスレッドを用いる並列処理っぽい。
///ーー 非同期処理 ーー///
// ある処理が終了するのを待たずに別の処理を並行/並列的に実行すること。複数の作業を同時に行う時に重い処理をバックグラウンドに移してアプリのフリーズを防いだり、大量のデータを扱うときに並列処理して処理時間を爆速に出来たりする。
// 特に、マルチコアプロセッサが普及した現代においては、並列処理を通じて複数のコアを効率良く活用しないと PCのパフォーマンスが最大限発揮できない。
// 後述する C++ で非同期処理する時によく使う std::thread や std::async は複数CPUスレッドを並列処理を行う非同期処理の関数。
// std::thread関数
// 戻り値が必要ない時の並列処理、#include <future> が必要。
int LongTask0() {
printf("LongTask0 開始\n");
/*~ 何かの重い処理とかを行う ~*/
std::this_thread::sleep_for(std::chrono::seconds(3)); // 例として呼び出し元のスレッドの処理を3秒停止させる
return 10;
}
void do_another_things() {}
int main() {
double a;
// 非同期タスクの開始。変数に格納してスタート
std::thread th1 = std::thread([&a] { a = LongTask0(); }); // 「 thread (関数or関数オブジェクト, その関数の引数...) 」の形
// メインスレッドでは他の処理を行える ~
do_another_things();
// ~ メインスレッドでは他の処理を行える
th1.join(); // thread.join(); でタスクの終了待ち。std::thread 使うときは必ず thread.join()が必要。
std::cout << a << std::endl; // 1.0
}
// std::async関数 と std::future関数
// 戻り値が必要な時の並列処理、新しいスレッドで実行た結果を std::future オブジェクトとして返す。#include <future> が必要。
int LongTask1() {
printf("LongTask1 開始\n");
/*~ 何かの重い処理とかを行う ~*/
std::this_thread::sleep_for(std::chrono::seconds(5)); // 例として呼び出し元のスレッドの処理を5秒停止させる
return 42;
}
void LongTask2(int x, float y) {
printf("LongTask2 開始\n");
/*~ 何かの重い処理とかを行う ~*/
std::this_thread::sleep_for(std::chrono::seconds(4)); // 例として呼び出し元のスレッドの処理を4秒停止させる
printf("Task2終了。結果: %f\n", x * y);
}
std::atomic<bool> isLongTask3End(false); // メインスレッドと別スレッドの2つのスレッドからアクセスされていて、片方が書き込みなので競合避けるために atomicにしている(atomicの説明は後述)
int LongTask3(int x) {
printf("LongTask3 開始\n");
/*~ 何かの重い処理とかを行う ~*/
std::this_thread::sleep_for(std::chrono::seconds(3)); // 例として呼び出し下のスレッドの処理を3秒停止させる
isLongTask3End = true;
return x;
}
std::atomic<bool> isCanceled(false); // メインスレッドと別スレッドの2つのスレッドからアクセスされていて、片方が書き込みなので atomicに。
void longRunningTask() {
int counter = 0;
printf("LongRunningTask 開始\n");
while (!isCanceled) {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "稼働中... " << counter++ << std::endl;
}
std::cout << "LongRunningTask キャンセル" << std::endl;
}
int main() {
// 非同期タスク➀
std::future<int> future1 = std::async(LongTask1); // std::future を std::async で初期化すると非同期処理開始。LongTask1関数を別スレッドで行う。
// メインスレッドでは他の処理を行える ~
std::this_thread::sleep_for(std::chrono::seconds(1));
printf("LongTask1 を待ってる間に他の事しちゃお~\n"); // 1秒待ってこんな文を出力する処理
// ~ メインスレッドでは他の処理を行える
printf("Task1終了。結果: %d\n", future1.get()); // std::future.get() で別スレッドに立てた非同期処理を待って結果を取得する。
// 非同期タスク➁
// std::async()のパラメは (ポリシー, 関数or関数オブジェクト, その関数の引数...)
// ポリシーは任意。関数、その関数の引数 は必須。ただしこの関数は「値渡ししかできない」ことに注意、参照渡し使用とすると Invoke系のエラー生じる(多分参照はスレッドを跨げない)。
// ポリシーは別スレッドで実行する std::launch::async、遅延評価する std::launch::deferred の2つが指定でき、デフォルトでは両方指定されている(遅延評価は後述)。
std::future<void> future2 = std::async(LongTask2, 2 * 2, 3.14);
// メインスレッドでは他の処理を行える ~
std::this_thread::sleep_for(std::chrono::seconds(1));
printf("LongTask2 を待ってる間にも他の事しちゃお~\n");
// ~ メインスレッドでは他の処理を行える
future2.wait(); // std::future.wait() で別スレッドに立てたの非同期処理の終了を待てる。(std::future.get と違い結果は取得しない。戻り値不要の場合は std::thread と同様の機能)
// 非同期タスク③
// 別スレッドで立てた非同期処理は別に待たなくても良いが、その処理が終了したかどうかの通知は必要になる(メインスレッドとかに完了した処理の影響が伝わらないと非同期処理が無意味なる)
std::future<int> future3 = std::async(LongTask3, 10);
while (!isLongTask3End) {
std::this_thread::sleep_for(std::chrono::seconds(1)); // 1秒ごとに終わったかチェック
printf("まだTask3 終わらんのぉ\n");
}
printf("Task3 終了\n");
// 非同期タスク④
// C++ 標準ライブラリには非同期タスクを直接キャンセルする機能はないので、各タスクの中で条件を管理してタスクを終了させる必要がある。
std::future<void> future4 = std::async(longRunningTask);
std::this_thread::sleep_for(std::chrono::seconds(5)); // 5秒後にタスクをキャンセル
isCanceled = true;
future4.wait();
// 非同期タスク⑤
std::vector<std::future<int>> futures; // 複数の非同期タスクを配列とかコンテナ系に入れて管理することも可能。
// 3つの非同期タスクを開始
for (int i = 0; i < 3; ++i) {
futures.push_back(std::async(LongTask3, i * 10));
}
// 終わり次第結果取得
for (auto& f : futures) {
printf("結果: %d\n", f.get());
}
}
// 遅延評価
// いくつか定義があるらしいけど、ここでは「値が使わるそのタイミングで初めてその値を評価すること」。不必要な値の評価を省略でき、また値の再利用とかもしてくれる最適化手法
// 普通は正格評価と言って、値を使う使わない構わず渡されたら評価される。
int foo(int a, int b) { return a; } // 二つ目の引数の b は使用されていないから別に要らない
int bar(int c) { return c - 1; }
int main() {
printf("%d", foo(2, bar(1))); // 正格評価だと不必要な bar(1) も先んじて計算される ← この部分を遅延評価だと省けるらしい。特に何もなければ std::async() の遅延評価はOnのままでいい
}
// 非同期タスクの例外処理
// 非同期処理絡みで生じるエラーはマジで複雑で難解になりやすいので、適切な例外処理とエラーハンドリングが重要になる。
// スレッドを跨いだ例外送出は、渡したいスレッド先で future.get() をtry-catchしないと出来ないっぽい。あるスレッドで例外を送出して他のスレッドで検知しようとしても、非同期タスクそのものを検知したり、future.wait() とか thread.join()で検知することはできなかった(明確に説明されてる文献はなかったけど、文献で例外取得説明してる例は future.get()のtry-catchしかしておらず、自分で他の試してみたら無理だった)。
void taskThatThrows() { throw 12; }
int main() {
try {
std::future<void> f0 = std::async([] {
try {
taskThatThrows();
}
catch (...) {
printf("f0:非同期スレッド内でエラーキャッチ\n");
throw;
}
});
}
catch (int) { // 非同期タスクのオブジェクト自体に catchかけても、別スレッドでは検知できない。
printf("f0 オブジェクトそのまま: メインスレッド内でエラーキャッチ\n"); // ←これは無理。
}
std::future<void> f1 = std::async([] {
try {
taskThatThrows();
}
catch (...) {
printf("f1:非同期スレッド内でエラーキャッチ\n");
throw;
}
});
try {
f1.get(); // std::future の非同期処理タスクを検知したいスレッドで .get() した時だけ、別スレッドでも catchで検知可能。
}
catch (int) {
printf("f1.get(): メインスレッド内でエラーキャッチ\n");
}
std::future<void> f2 = std::async([] {
try {
taskThatThrows();
}
catch (...) {
printf("f2:非同期スレッド内でエラーキャッチ\n");
throw;
}
});
try {
f2.wait(); // future.wait() だと検知できない。.get()の時だけ可能っぽい
}
catch (int) {
printf("f2.wait(): メインスレッド内でエラーキャッチ\n");
}
}
// async await を用いたコルーチンってなんだってばよ、、
///ーー 非同期処理のコンフリクト解決・排他制御 ーー///
// マルチスレッドにおける非同期処理はデフォでは各々の処理が自由に動くため、同一のリソースを共有している場合、一方が処理中のタイミングで他スレッドで書き込みが生じると値の整合性が取れなくなり、動作の度に値がばらつくようになる(全て読み込みだけの場合のみ同時にアクセスしてもほとんど問題はない)。
void do_another_things() {}
int main() {
int a = 0;
// 非同期タスクの開始
std::thread th1 = std::thread([&a] { a += 1; printf("+1したよ\n"); });
std::thread th2 = std::thread([&a] { a *= 2; printf("x2したよ\n"); });
// メインスレッドでは他の処理を行える ~
do_another_things();
// ~ メインスレッドでは他の処理を行える
th1.join();
th2.join();
std::cout << a << std::endl; // a は 1 になったり 2 になったりする。
// 記述した順に th1 → th2 の順で非同期タスク開始するが、中の処理が進んでいくタイミングも終わるタイミングもバラバラになる(似たような四則演算でも色んな影響を受けて速度がばらつく)。
// この例だとシンプルに th2 の開始前に th1 の処理を待てば良いだけだが、複数のタスクが各々のタイミングで非同期に開始されている状況では、全てのタスクを網羅的に待ち管理するのは不可能。
}
// 排他制御
// 上記の値の不整合を避ける方法の1つ。1つのスレッドが或るリソースにアクセスしている最中に、同じリソースにアクセスしようとする他のスレッドを処理待ち状態にして、複数のスレッドが同一のリソースに同じタイミングでアクセスすることを防ぐ。
// C++ では std::mutex が排他制御のための標準機能を備えている(ただし割と重い)。 #include <mutex> が必要。
// std::mutex は、ロックとアンロックの2つの主な操作を持ち、mutex オブジェクトをロック状態にするとそれがアンロックされるまで、他のスレッドにある同じ mutex オブジェクトを使っているロック以降の式を待機させられる。
int plus1(int a) { // 関数の形で使う場合も見せたいのでこの形にしたが、上記の例とやってることは同じ。値渡ししかできないので返り値を返してラムダの中で代入する必要がある。
a += 1;
printf("+1よ\n");
return a;
}
int multiple2(int a) {
a *= 2;
printf("x2よ\n");
return a;
}
int main() {
int a = 0;
std::mutex mtx1; // std::mutex のインスタンス.lock() / .unlock でその mutex のロック/アンロック可能。
std::future<void> f1 = std::async([&mtx1, &a] {
mtx1.lock();
a = plus1(a);
mtx1.unlock(); // この処理が終わってアンロックされるまで、他のスレッドで mtx1 を使ったロック以降の処理を強制待機。
});
std::future<void> f2 = std::async([&mtx1, &a] {
mtx1.lock(); // 他のスレッドで mtx1 にロック状態が先に生じてたら、アンロックされるまで以降の処理を強制待機させられる。
a = multiple2(a);
mtx1.unlock();
});
f2.wait();
std::cout << a << std::endl; // a は確実に 2 になる。
// 記述した順に f1 → f2 の順で非同期タスク開始する。f1 f2 内の最初に mtx1 の lock があり、f1の方が早いので f2 内の lock 以降の処理は f1 で unlock された後しか実行できない。
// std::lock_guard<class mutex> を使うと生成時と破棄時に自動でロックアンロックを行える。
// テンプレート部分の mutex クラスは std::mutex でなくても pulicメンバに lock(), unlock()を持っているクラスなら何でもOK。
// ブロックスコープの先頭で mutex からオブジェクトを生成して、その時に自動で mutex の lock() を呼び出し、そのブロックスコープを抜けるときに自動で unlock を呼び出す。
// アンロック漏れを回避できて、メモリや処理のオーバーヘッドもほぼゼロで軽い。単体で使うときはこのロックを使うことを推奨。
std::mutex mtx2; // mutex オブジェクト毎にロックアンロック管理されるので、この mtx2 は上記 mtx1 とは無関係にロックされる。
{
std::lock_guard<std::mutex> lock(mtx2); // std::lock_guard オブジェクトを生成して mtx2 をロック。
do_critical_session();
// 他のスレッドで mtx2 を使ってロックできない
} // デストラクタで自動にアンロックされる
}
// std::unique_lock は上記 std::lock_guard の拡張版で、複数の操作性がある一方 lock_gurad に比べて処理が重くなる。
// 「コンストラクタに限らずあとから lock() できる」「ロック取得の成否を返す try_lock() を使用できる」「所有権を移動(move)・交換(swap)・放棄(release)できる」などの機能がある。拡張機能の詳細→ https://cpprefjp.github.io/reference/mutex/unique_lock.html
// 内部で unique_lock 使ってロックした mutex を返す関数を使うと、呼び出し側のスコープ終了時にアンロックされるようにできる。
class UniqueLock1 {
private:
int value_ = 0;
mutable std::mutex mtx_;
private:
// この関数の中でロック
std::unique_lock<std::mutex> get_locked_mutex() const {
return std::unique_lock<std::mutex>(mtx_); // ロックして返す。ロック済み mutex を返すのは lock_guard では無理(返すときにアンロックされる)。
}
public:
void add_value() {
std::unique_lock<std::mutex> lk = get_locked_mutex(); // ロック済み mutex を返せる。
++value_;
} // 呼び出し側のスコープ終了でアンロックされる
int get() const {
std::unique_lock<std::mutex> lk = get_locked_mutex();
return value_;
}
};
int main() {
UniqueLock1 ul1;
std::thread th1([&] { ul1.add_value(); });
std::thread th2([&] { ul1.add_value(); });
th1.join();
th2.join();
std::cout << ul1.get() << std::endl; // 2
}
//---------------
// ロックした mutex を関数の引数に渡すと、渡した先(呼び出した先)の関数のスコープ終了時にアンロックされるようにできる。
class SpinlockMtx { // mutex でロックアンロック時にログ流れるようにした mutex。スピンロックはスレッドがロックを獲得できるまで単純にループして定期的にロックをチェックしながら待つ方式
private:
typedef enum { Locked, Unlocked } LockState;
std::atomic<LockState> state_;
public:
SpinlockMtx() : state_(Unlocked) {} // 最初は Unlock スタート
void lock() {
// 現在の状態を Locked と入れ替える。返り値で元の値が返る。
while (state_.exchange(Locked) == Locked) { /* 既に Locked の場合は、一度 Unlock になるまでループで待機 */ }
std::cout << "now locked" << std::endl;
}
void unlock() {
// 現在の状態を Unlocked に変更
state_.store(Unlocked);
std::cout << "now unlock" << std::endl;
}
};
class UniqueLock2 {
private:
int value_ = 0;
mutable SpinlockMtx m_mtx; // std::lock_guard<> や std::unique_lock<> のテンプレート部分の mutex クラスは、別に std::mutex じゃなくても、public メンバに lock(), unlock() 関数を持っているクラスなら何でもOK
private:
std::unique_lock<SpinlockMtx> get_locked_mutex() const { // この関数の中でロック
return std::unique_lock<SpinlockMtx>(m_mtx); // ロックして返す。ロック済み mutex を返すのは lock_guard では無理(返すときにアンロックされる)。
}
void add_value_impl(std::unique_lock<SpinlockMtx>&& mtx) { // ロックを受け取って
++value_;
std::cout << "add valued" << std::endl;
} // ここでアンロックされる
public:
void add_value() {
add_value_impl(get_locked_mutex()); // ロック済みミューテックスを関数の引数に渡す
}
int get() const {
std::unique_lock<SpinlockMtx> lk = get_locked_mutex();
return value_;
}
};
int main() {
UniqueLock2 ul2;
std::thread th1([&] { ul2.add_value(); });
std::thread th2([&] { ul2.add_value(); });
th1.join();
th2.join();
std::cout << ul2.get() << std::endl;
}
//---------------
// 条件変数 std::condition_variable / std::condition_variable_any
// ここでは簡単に、排他制御と組み合わせて使う方法に限定する、他の使い方が気になるなら調べて。#include <condition_variable> が必要。
// 条件変数は、ロック型オブジェクトを対象として処理フローの一時待機と通知による再開を行うことで、非同期処理の「処理順序の管理」や「処理の待機」を行える。
// stdの条件変数は2種類あって、std::condition_variable の場合は std::unique_lock のみを対象とし、std::condition_variable_any の場合は任意の ロック型オブジェクトを対象にできる。
// 条件変数と排他制御の組み合わせをするには、「条件変数オブジェクト」「mutex/ロック型オブジェクト」「待機/再開の状態に関わる変数群」の3要素が必要になる。
/* よくある実装パターン
std::condition_variable cv; //「条件変数オブジェクト」
std::mutex mtx; //「mutex/ロック型オブジェクト」
int state; //「待機/再開の状態に関わる変数群」(変数型やその個数は目的による)
//「待機/再開の状態に関わる変数群」の更新+通知。なんか関数内とか。
{
std::lock_guard<std::mutex> lk(mtx); // ロック
// ~「待機/再開の状態に関わる変数群」の更新処理~
cv.notify_all(); // 通知
}
// 「待機/再開の状態に関わる変数群」が条件を満たすまで待機。なんか関数内とか
{
std::unique_lock<std::mutex> lk(mtx); // ロック
cv.wait(lk, [&] { ~「待機/再開の状態に関わる変数群」を元に再開していいかT/Fで返す処理~ });
// ~Tの時再開される処理~
}
*/
// 非同期で要素の挿入と取り出しを行い、要素の数に応じて処理を待機する Queue の例
template<typename T, size_t N>
class bounded_queue {
std::condition_variable not_empty_;
std::condition_variable not_full_;
std::mutex m_mtx;
std::queue<T> m_queue;
public:
// 要素の挿入
void push(T val) {
printf("push start\n");
std::unique_lock<std::mutex> lk(m_mtx); // ロック
//「条件変数.wait( 既にロック中のロック型オブジェクト, 「待機/再開の状態に関わる変数群」を元に再開していいかを bool で返す関数オブジェクト)」
// まずこの文に到達した時に、第二引数の関数オブジェクトが ture を返すかどうかを判定する。true を返したときは何もせずそのまま処理フローを進める。
// false が返ってきた時だけ、第一引数の既にロックされていたロック型オブジェクトを一度アンロックしてこのスレッドの以降の処理フローを待機させる。同じ条件変数が通知を受けた時に、再び第二引数の関数オブジェクトが ture を返すかの判定がなされる。この判定で ture を返すと第一引数を再びロックして待機していた処理フローを再開する。
// (実は第二引数を省略したやり方もできるけど、Spurious Wakeup っていう良く分からん不具合生じるから、安全のためにも記述した方がいい)。
not_full_.wait(lk, [this] { return m_queue.size() < N; });
m_queue.push(std::move(val));
printf("push done\n");
//「条件変数.notify()」でその条件変数オブジェクトに再開を通知。
// スレッドの待機と再開通知は Queue 状に積まれて対処されていき、notify_one は1つ、notify_all は全部の待機しているスレッドに対して通知する。前者の方が処理は軽い。
// 通知が待機よりも先になると待機が一瞬で解除されるようになるだけで問題はない。ただし、通知が無い or されないと待機は一生終わらなくなる (live lock) 。
// そのため、notify_one は notify_all よりも処理が軽いが、通知漏れがないかを十分注意する必要がある。
not_empty_.notify_one();
}
// 要素の取り出し
T pop() {
printf("pop start\n");
std::unique_lock<std::mutex> lk(m_mtx); // ロック
not_empty_.wait(lk, [this] { return !m_queue.empty(); }); // .wait()
T ret = std::move(m_queue.front());
m_queue.pop();
printf("pop done. no = %d \n", ret);
not_full_.notify_one(); // .notify()
return ret;
}
};
int main() {
bounded_queue<int, 1> q;
std::thread th1([&] { q.push(11); });
std::this_thread::sleep_for(std::chrono::seconds(1));
std::thread th2([&] { q.push(12); }); // 要素数が満杯なので、減るまで待機
std::this_thread::sleep_for(std::chrono::seconds(1));
q.pop();
th1.join();
th2.join();
}
//---------------
// 1つの条件変数で複数のスレッド待機を管理する例
std::mutex mtx;
std::condition_variable cv;
bool is_ready = false;
void PrepareProcess() {
std::cout << "Start Preparing" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3));
std::cout << "Finish Preparing" << std::endl;;
{
std::lock_guard<std::mutex> lock(mtx);
is_ready = true;
}
cv.notify_all(); // .notify_one() だと最初に待機したスレッドにしか通知されず、LiveLockになる
}
void DoProcess1() {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Start Thread1" << std::endl;
{
std::unique_lock<std::mutex> uniq_lk(mtx);
cv.wait(uniq_lk, [] { return is_ready; });
}
std::cout << "Finish Thread1" << std::endl;
}
void DoProcess2() {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Start Thread2" << std::endl;
{
std::unique_lock<std::mutex> uniq_lk(mtx);
cv.wait(uniq_lk, [] { return is_ready; });
}
std::cout << "Finish Thread2" << std::endl;
}
int main() {
std::thread th_prepare([&] { PrepareProcess(); });
std::thread th_main1([&] { DoProcess1(); });
std::thread th_main2([&] { DoProcess2(); });
th_prepare.join();
th_main1.join();
th_main2.join();
}
// 正直、1条件変数に1スレッドだけ任して個々に notify_one すべきか、1条件変数に複数スレッド任せてまとめて notify_all すべきか余りわかんない(どっちでも実装できる)。
// 排他制御時の例外処理
// mutex オブジェクトが破棄されるときは、必ずアンロックされた状態でないと abort() が生じるので、処理が中断される例外処理の時は注意が必要。
void RiskyFunc() {
std::mutex mtx;
mtx.lock();
try {
/*~ なんか危ない処理 ~*/
throw string("エラー");
}
catch (string& str) {
mtx.unlock(); // 例外はいちゃったとき用のアンロック
std::cout << str << std::endl;
throw;
}
mtx.unlock(); // ちゃんと通ったとき用のアンロック。std::lock_guradを使えば自動でアンロックしてくれるからこれらは不必要になる。使用推奨。
}
int main() {
std::future<void> f2 = std::async([] {
try {
RiskyFunc();
}
catch (...) {
std::cout << "同じスレッド内なら再送出した例外も補足できる" << std::endl;
throw; // 例外の送出と検知はスレッドをまたがないので無意味。
}
});
}
// デッドロックの回避
// 複数の mutex オブジェクトが互いのロック解放を待ち続ける状況をデッドロックといい、プログラムが止まる。安易に排他制御を多用するとこれが生じる可能性が高くなる。
// これを回避するためには、ロックの取得順序を正しく制御したり、ロックの範囲を最小限にしたり、不要なロックを避けたり、タイムアウトを設定するなどの方法が有効。しかし、複数スレッドが関わっていたりコード内容が複雑になるほど、ロックの取得順序を制御したり範囲を狭めたりすることが難しくなっていく。
int main() { // デッドロックの例
mutex m1, m2;
// 非同期スレッド1開始
thread thread1([&] {
lock_guard<mutex> l1(m1); // m1 をロック
this_thread::sleep_for(std::chrono::seconds(2)); // なんか長い処理
{
lock_guard<mutex> l2(m2); // m1 ロックの中で m2 をロックしようとしてるが、m2 をロックする前に長い処理があって、その間に m2 は thread2 で既にロックされている
}
}); // thread1 による m1 ロックはここに来ないと解除されない
// 非同期スレッド2開始
thread thread2([&] {
lock_guard<mutex> l1(m2); // m2 をロック
this_thread::sleep_for(std::chrono::seconds(2)); // なんか長い処理
{
lock_guard<mutex> l2(m1); // m2 ロックの中で m1 をロックしようとしてるが、m1は thread1 で既にロックされている
}
}); // thread2 による m2 ロックはここに来ないと解除されない
thread1.join();
thread2.join();
}
// 排他制御の欠点
// ・多くのスレッドからアクセスされているリソースを排他制御する時に、大幅にパフォーマンスが落ちてしまう。例えば、10個のスレッドからアクセスされている要素を排他制御すると、残り9個のスレッド全てをその間停止することになり、また 10個の内どれか1つのアクセスが生じる度に排他制御が生じるので発生頻度が高くなってしまう。
// ・std::lock 系の処理がそもそも重いので多用すると普通に負荷がかさばる。
// ・デッドロックの可能性があり、また複数スレッドを管理している場合はその回避の難易度が高くなる。
// 上記の理由から、排他制御は使わなくて済むならできるだけ使わないようにした方がパフォーマンスも安全性も良くなる。
// 排他制御を避けるやり方としては、タスクをスレッドに分担するときに関連する処理を1スレッドにまとめてそれらを順序に同期処理させることでスレッド間競合を減らす、コピーバッファを用意して排他制御を減らす、そもそも変数の用途を読み込みに限定させて排他制御を不要にするといったことをおこなうとでも、ロックフリーな競合回避アルゴリズムを使うなどがある(ロックフリーなコンフリクト解決は後述)。
///ーー ロックフリーなコンフリクト解決 ーー///
// 上述の通り、排他制御は要所要所で使うなら強力だが、同じリソースを共有するスレッド数が増えるほど、パフォーマンスが大幅に低下する可能性が生じる。
// これを避けるシンプルなやり方としては、タスクをスレッドに分担するときにリソースを同じくする処理を1スレッドにまとめて各スレッド内で順番に同期処理させることでスレッド間でのリソースの共有を減らす、コピーバッファを用意して排他制御を減らす、そもそもリソースの使用を読み込みに限定させて排他制御を不要にする、などがある。
// これらの設計を工夫して競合を避ける方法は、最も安全でパフォーマンスも良いが設計に制限が生じやすい。一方で、ロックフリーアルゴリズムを用いた方法は、難解な設計にはなりやすいが排他制御を用いずにマルチスレッドで自由度が高い設計を可能にする。
// std::atomic を使った処理はロックフリーの中で比較的わかりやすい上、std::mutex や std::lock よりも負荷が軽い(処理の速さ:両方使わない工夫した設計 > std::atomic > std::mutex/std::lock 。安全性:両方使わない工夫した設計 ≧ std::mutex/std::lock ≧ std::atomic。なので、非同期処理の設計時は、排他制御もロックフリーも使わない工夫した設計 > ロックフリーを使う設計 > 排他制御を使う設計 を試みる)
// メモリーダー問題の解決
// ロックフリーでコンフリクトを解消しようとするとメモリオーダーの管理に問題が発生する可能性が生じる。
// 環境依存だったり、最適化によるコンパイルで、プログラム順とメモリアクセス順(メモリオーダー)が同一にならないことがある。これは単一スレッドのみが動作している場合は問題ないが、複数のスレッドが動作している時はそれら順序の関係で値の整合性が取れなくなったりする。
// この不具合を防ぐことをメモリバリアといい、std::atomic はメモリアクセス順をコントロールしてメモリバリアを可能にする。atomic の第二引数でその形式を管理でき、デフォルトでは逐次一貫性(メモリ操作が全てのスレッドからプログラム順と同じ順で観測できる(同時ではなく見かけ上順番に行われるようになる))を実現するようになっており、これは指定できる中でも最も強い保証になる。他のメモリオーダーの指定は特定の条件で機能を限定して高速化出来るオプションらしい(例えば .storeで読み込みのみの時は memory_order_release使うとか)が、正直細かくどのメモリーオーダーがどれほど作用するのか系は抽象的な理論の話多すぎて何言ってるかわからなかったから、デフォでいいと思う。(https://nodamotoki.hatenablog.com/entry/2015/11/02/022700 , https://cpprefjp.github.io/reference/atomic/memory_order.html)
// まとめると、とりま非同期処理する時に共通のリソースを使おうとするとコンフリクトが生じる可能性がある。そしてそれをロックフリーの形で解決しようとすると、メモリオーダー関連で不具合が発生する可能性があって、atomic はそれを解消できるよ、、ということかな?多分。つまり、atomic を使ってロックフリーを実現するためには、共通リソースに atomic オブジェクトを使い、かつコンフリクトを解決する処理を書く必要があるっぽい(なんか共通リソースを atomic オブジェクトにしただけでは、別に競合は解決されない、、.load() / .store()を使っても無理やった、、)。
// コンフリクトの解決(メモリオーダーの問題は atomic オブジェクトにすることで解決↑)
// 次にコンフリクトを解決する方法としては、メモリから値を読み込み、それを処理した後に、再度読み込み元のメモリを確認して、処理中に読み込み元の値が変化していなければ書き込みが行われる CAS(Compair and Swap)という方法がある。しかし CASには、仮に処理中に何らかの更新が生じたとしても、読み込み元の値が処理の前後で差がなければ競合はない(偽陽性FalsePositive)と判断されてしまう問題がある。
// 一方でより強力な LL/SC法は、たとえ読み込み元の値が処理前後で同じ内容になっていたとしても、何らかの更新がなされていたら書き込みを失敗させることができる。
// LL/SC法のうち、読み書きの操作の間に何らかの例外事象(コンテキストスイッチ、別の読み込みまたは書き込み操作、2つの操作間の例外イベントなど)が発生することで、そのメモリへの更新がないときでも誤って失敗する(Spuriously Fail)可能性を含む不完全な LL/SC法を weak LL/SCと呼ぶ。
// std::atomic では前者 完全な LL/SC と後者 weak LL/SC をそれぞれ実現した compare_exchange_strong() と _weak() 関数が用意されている。
// weak の方が色んな環境においてパフォーマンスが良くなる可能性が高く、またループさせる構造にすれば weak でも誤った失敗(Spuriously Fail)による不具合を避けられるため、基本的には成功するまでループさせる構造にして weak の方を使い、ループ構造にも出来ず Spuriously Fail も許容できない状況のみ strong を使う。
// atomic オブジェクトと strong/weak の例
int main() {
// atomic オブジェクト生成「std::atomic<クラス名> 変数名」
// ただしこの クラス名 は、コンストラクタ/デストラクタ/コピコン/ムブコン/コピ代入/ムブ代入全てが暗黙定義されているクラスしか扱えないことに注意
std::atomic<int> x = 3;
/* ~なんか色んな処理or非同期による処理~ */
// LL/SCによる書き込み「➀書き込みたいatomicオブジェクト.compare_exchange_weak/strong (➁atomicオブジェクトが読みこまれた時にそうであって欲しい値, ➂書き込む値)」
// ➀ == ➁の時に書き込み成功。成功時には➀を➂で置き換える。失敗時には逆に期待する値➁が現在の値➀で置き換えられる。最後に返り値として書き込みの成否をboolで返す。
int expected1 = 3;
bool result1 = x.compare_exchange_weak(expected1, 2); // x が読み込まれた時の値 == expected1なので、xは2に置き換えられる
std::cout << (result1 ? "x 書き換え成功" : "x 書き換え失敗") << ":x = " << x.load() << ":expected1 = " << expected1 << std::endl; // atomic オブジェクトの読み込み「-.load()」
/* ~なんか色んな処理or非同期による処理~ */
int expected2 = 1;
bool result2 = x.compare_exchange_weak(expected2, 2);// x が読み込まれた時の値 != expected2なので、expected2がxの値で置き換えられる
std::cout << (result2 ? "x 書き換え成功" : "x 書き換え失敗") << ":x = " << x.load() << ":expected2 = " << expected2 << std::endl;
}
// 実際に非同期処理として競合を避けた使い方の例
// 非同期下でも競合なくアトミックにインクリメントできるクラス
class AtomicCounter {
std::atomic<int> m_count = 0;
public:
// インクリメント。std::atomic<int>::fetch_add(1)とも同じ
void increment() {
int expectedValue = m_count.load(); // 読み込みの時の値
int writingValue = 0;
// 最新の値に依存して処理をしたい時には do・whileループを使った形をよくとる
do {
writingValue = expectedValue + 1;
// 読み込みから書き込みまでのこの間に、他のスレッドによって m_count の値が書き換わっている可能性があるため、繰り返しで最新値を探る
// m_count != expectedValue の場合 expectedValue = m_count に更新される
// Spurious Failureなら変数たちの値は変わらずそのままもう一度
} while (!m_count.compare_exchange_weak(expectedValue, writingValue));
}
// 値の上書き
void store(int new_value) {
// 最新の値に依存しない場合は、Spurious Failureを回避するためのwhile文のみ必要
int expectedValue = m_count.load();
// Spurious Failureならそのままもう一度、m_count != expectedValue の場合 expectedValue = m_count に更新されてもう一度なので、実質ただの Spurious Failure避けるだけの書き込み
while (!m_count.compare_exchange_weak(expectedValue, new_value)) {}
}
// 値の読み込み
int load() const {
return m_count.load();
}
};
int main() {
AtomicCounter x;
// 複数スレッドでインクリメントを呼んでも最終的に全てのインクリメントが処理された値になる
std::thread t1{ [&x] { x.increment(); } };
std::thread t2{ [&x] { x.increment(); } };
t1.join();
t2.join();
std::cout << x.load() << std::endl;
}
///ーー スレッドプール ーー///
// 並列処理をするときには、メインスレッドから適切な数だけ派生するスレッドを立てて、タスクを割り振ることで効率化を図る。
// スレッドの生成と破棄にはコストがかかるので、非同期タスクを行うたびにスレッドの生成と破棄を繰り返すのは非効率極まりない。そこで、スレッドを作成した後に待機状態にして、非同期処理をしたいタスクを都度割り振っていくスレッドプール(Thread Pool/Worker Thread)という形をよく取る。並列処理なので大抵は全CPUスレッド数分まではスレッドを用意した分だけ処理が高速化する。
// C++ には標準機能でスレッドプールの機能が存在しないので、自分で作るか外部ライブラリ使う必要がある。外部ライブラリをそのまま使ってもいいが、流石に中身を知らずに使うのはやばいのと、自作出来た方がカスタマイズできたり標準でサポートされていない他言語へ対応できるため学ぼう。https://contentsviewer.work/Master/software/cpp/how-to-implement-a-thread-pool/article みて
// 下記の実装例では、以下のような仕組みになっている
// ➀メインスレッドからスレッドプールオブジェクトを作成と同時に起動する。
// ➁メインスレッドからスレッドプールにタスクを関数オブジェクトの形として渡し、その際にメインスレッドは渡したタスクを std::future オブジェクトとして管理する。
// ➂スレッドプール自体の仕組みは、待ち行列(Queue)にタスクを積んでいき、積まれたタスクをスレッドの集団(スレッドプール)の中で空いているものに割り当てていく。各スレッドは各々の処理が終わり次第 Queueに積まれたタスクが無くなるまで再びタスクを引き取って処理していく。各スレッドは Queueに積まれたタスクがなくなると、新しくタスクが割り振られるまでスレッドを待機させる。
// ④スレッドプール側で処理が終わったタスクは、メインスレッドの std::future オブジェクトから結果を得ることが出来る。
// ➀メインスレッド
// ↓
// ↓→→→→→→ ➀スレッドプール
// ↓ ↓
// ↓ ↓
// ↓ ↓
// ➁タスク →→→→→ ➂ Queue に積む ←←←←←←←←←
// ↓ ↓ ↑
// ➁futureが残る ↓ ➂積まれたものを受け取り無ければ待機
// ↓ ↓ ↑
// ↓ ➂各スレッドで処理 →→→→→→→→
// ↓ ↓
// ↓ ↓
// ④futureを見る(待つ) ←←← 処理が終了
namespace concurrent {
class ThreadPool {
private:
using ui32 = std::uint_fast32_t; // using で長い型名を変換
const ui32 m_threadCount; // スレッドプールで管理するスレッドの数
std::unique_ptr<std::thread[]> threads; // 管理するスレッドたちの配列スマポ(初期化なし)
std::queue<std::function<void()>> taskQueue{}; // タスクを積む Queue。function 使ってるけど、他の関数オブジェクトにした方が高速にできる、、、よな?多分。
mutable std::mutex queueMutex; // 複数スレッドからの Queue へのアクセスを同期させる mutex。排他制御を使ってるけど、atomic とかロックフリーにすればより高速化できる?
std::atomic<bool> isTPRunning{ true }; // スレッドプールが稼働中か終了中かの状態を表す。これ書き込む時毎回同じ mutex ロックしてて排他制御成立してるから、ここでは atomic でなくてOK
std::condition_variable condition; // 各スレッドの処理の待機を管理する条件変数
public:
// コンスト。スレッドの作成
// std::thread::hardware_concurrency() で処理系にサポートされているCPUスレッド数を取得する。失敗すれば0が返る。
ThreadPool(const ui32& ThreadCount = std::thread::hardware_concurrency())
: m_threadCount(ThreadCount ? ThreadCount : std::thread::hardware_concurrency()) // 0指定か失敗した時にもう1回取得
{
threads.reset(new std::thread[m_threadCount]); // リセットでスマポ初期化
for (ui32 i = 0; i < m_threadCount; ++i) {
threads[i] = std::thread(&ThreadPool::worker, this); // 作った各スレッドを起動して worker() を実行。worker()は タスクの取り出し, タスクの実行など を行う?
}
}
// デスト。taskQueue へのタスクの追加を止め、残った全てのタスクを処理するのを待つ。
~ThreadPool() {
{
std::lock_guard<std::mutex> lock(queueMutex); // この間に Queue にタスク追加させないように、queueMutex をロック(別の mutex は使わない)。
isTPRunning = false; // スレッドプールを終了中に。
}
condition.notify_all(); // なんらかの影響でタスクがあるのに止まっているスレッドがあった時用に、一旦全部のスレッドに通知
for (ui32 i = 0; i < m_threadCount; ++i) { threads[i].join(); } // 全てのスレッドの終了を待つ
}
ui32 ThreadCount() const { return m_threadCount; }
// メインスレッドからスレッドプールに、任意の引数と1つの返り値を持つ関数オブジェクトをタスクとして追加し、std::future 型のオブジェクトを返す関数。
// メインスレッド側では、std::future オブジェクトでタスクを管理するため、引数の数は0~任意だが、返り値は必ず1つ必要。
#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || __cplusplus >= 201703L) // C++17 と C++14 以前とで書き方が異なる。
// C++17 から推奨されている, 関数の返り値の型R を取得する方法
template <typename F, typename... Args, typename R = std::invoke_result_t<std::decay_t<F>, std::decay_t<Args>...>>
#else
// C++14 で, 関数の返り値の型R を取得する方法
template <typename F, typename... Args, typename R = typename std::result_of<std::decay_t<F>(std::decay_t<Args>...)>::type>
#endif
std::future<R> submit(F&& func, const Args&&... args) {
// packaged_task は future と組み合わせて「別スレッドでの処理完了を待ちその処理結果を取得する」非同期処理を実現する。
auto task = std::make_shared<std::packaged_task<R()>>([func, args...]() { return func(args...); });
auto future = task->get_future(); // packaged_task に登録した関数の戻り値を future で読み取る
push_task([task]() { (*task)(); }); //(ラムダ式で入れてるから、std::function じゃなくてもいけるよな、、? function 重かったはずだから変えたい)
return future;
}
private:
// スレッドプール内部で taskQueue にタスクを追加する関数。任意の関数オブジェクトを使えるようにジェネリックに。
template <typename F>
void push_task(const F& task) {
{
const std::lock_guard<std::mutex> lock(queueMutex);
if (!isTPRunning) { // スレッドプール終了時
throw std::runtime_error("スレッドプール終了中。新しいタスクは追加できません。");
}
taskQueue.push(std::function<void()>(task)); //(使うところでラムダ式で入れてるから、std::function じゃなくても多分いける function 重いから変えたい)
}
condition.notify_one(); // 待機中のスレッド1つに再開を通知。1つの追加時に1つ再開されるようになる。
}
// 各スレッドで動き続ける関数。タスクの取り出しと実行を行う。
void worker() {
for (;;) { // 無限ループ
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queueMutex); // queueMutex をロック
// スレッドプールが稼働中で taskQueue が空の場合、mutexをアンロックしてこのスレッドのフローを通知が来るまでここで待機(通知が来たときに再判定する)。そうでない場合は、ロックしたまま処理フローを再開する。
condition.wait(lock, [&] { return !(isTPRunning && taskQueue.empty()); });
// スレッドプールが終了中で taskQueue が空の時、ループを抜けてスレッドを終了させる
if (!isTPRunning && taskQueue.empty()) {
return;
}
// taskQueue が空でない時、taskQueue の先頭をムーブして取り出し。(スレッドプールが終了中でも、Queue にタスクが残ってたらやるっぽいな、、。)
task = std::move(taskQueue.front()); // 先頭要素アクセス
taskQueue.pop(); // 先頭要素削除
}
task(); // ロック解除後に実行。排他制御を無駄に続かせない。
}
}
};
}
// 使用例
class HelloWorld {
public:
std::string say_hello(int number) {
std::cout << "スレッドID " << std::this_thread::get_id() << ":say_hello タスク Start" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(2000)); // 何か重い処理
std::ostringstream oss; // std::ostringstream で << のストリームの形を保持。 .str() でそのまま string に変換できる
oss << "Hello! number " << number << ".(スレッドID " << std::this_thread::get_id() << "で実行)";
std::cout << "スレッドID " << std::this_thread::get_id() << ":say_hello タスク End" << std::endl;
return oss.str();
}
};
std::string say_ok(int number) {
std::this_thread::sleep_for(std::chrono::milliseconds(1000)); // say_hello と文字が被ると見にくいので遅らせ
std::cout << "スレッドID " << std::this_thread::get_id() << ":say_ok タスク Start" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(3000)); // 何か重い処理
std::ostringstream oss;
oss << "OK! number " << number << ".(スレッドID " << std::this_thread::get_id() << "で実行)";
std::cout << "スレッドID " << std::this_thread::get_id() << ":say_ok タスク End" << std::endl;
return oss.str();
}
int main() {
concurrent::ThreadPool threadPool; // スレッドプールの作成
std::cout << "利用可能なスレッド数: " << threadPool.ThreadCount() << std::endl;
auto ok_future = threadPool.submit(say_ok, 100); // グロ関数をスレッドプールにタスクとして追加、返ってきた future オブジェクトを取得。
HelloWorld hello;
auto hello_future = threadPool.submit([&](int number) { return hello.say_hello(number); }, 999); // メンバ関数をスレッドプールにタスクとして追加、返ってきた future オブジェクトを取得
std::cout << ok_future.get() << std::endl; // future オブジェクトから .get() でタスクの結果を取得
std::cout << hello_future.get() << std::endl;
}
///ーー ファイルの読み書き ーー///
#include <stdio.h>
#include <stdlib.h>
// ファイル読出しの問題点。
// 確保するメモリについて、書き込むときにはファイルサイズが分かるが、読み込むときにファイルサイズの大きさが分からない問題がある。
// fseek()関数、ftell()関数を用いてファイルサイズを知る必要がある。
int main() {
FILE* file;
int i, size;
char* rdata;
// バイナリデータの読み出し
file = fopen("C:¥¥test¥¥test.bin", "rb");
if (file == NULL) {
printf("ファイルオープンに失敗しました。¥n");
exit(1);
}
fseek(file, 0, SEEK_END); // ファイルの読み出し位置を最後まで移動
size = ftell(file); // ファイルの大きさを取得。これは読みだし位置を最後まで移動させないとできない。
rdata = new (nothrow) char[size]; // ファイルサイズだけ、配列を動的に生成
assert(rdata != nullptr);
fseek(file, 0, SEEK_SET); // ファイルの読み出し位置を最初に移動
fread(rdata, sizeof(char), size, file); // ファイルを読み込み
fclose(file); // ファイルを閉じる
for (i = 0; i < size; i++) {
printf("%x ", rdata[i]);
}
printf("¥n");
delete[] rdata;
rdata = nullptr; // メモリ解放
}
///ーー アライメントとパディング ーー///
// C++ のコンパイラはメモリにデータを書き込む際、書き込むデータの大きさや先頭の位置(アドレス)を、装置の管理単位の整数倍になるように調節することで処理効率を向上させている。
// ネイティブなデータ構造は予め上手くアライメントされているが、ユーザー定義のものはコンパイル時に行われるため、構造体やクラスのメンバ変数を上手く配置することでデータ型のサイズを減らせる
class BadAlignment {
// アライメントは定義されたメンバ順に上から詰めて行われる。最も大きな横幅を持つ型に全体の幅が揃えられ、足りない部分はメモリが空けられて(パディング)、実態が無いのに無駄になる。
// このクラスは、実態は 7 バイトだが、アライメントによって 12 バイトのサイズになっている。
char char1; // [ char1 ][ - ][ - ][ - ]
int int1; // [ int1 ][ int1 ][ int1 ][ int1 ]
char char2[2]; // [ char2 ][ char2 ][ - ][ - ]
};
class GoodAlignment {
// メンバ変数の定義の位置を変更するとアライメントが変わり、上のクラスと同じ内容だが 8 バイトのサイズになる。
int int1; // [ int1 ][ int1 ][ int1 ][ int1 ]
char char1; // [ char1 ][ char2 ][ char2 ][ - ]
char char2[2];
};
class Alignment2 {
// 配列は全体の大きさではなく要素一つ一つで換算されるため、int2[3] の幅は 12 ではなく 4
int int2[3]; // [ int2 ][ int2 ][ int2 ][ int2 ]
// [ int2 ][ int2 ][ int2 ][ int2 ]
// [ int2 ][ int2 ][ int2 ][ int2 ]
int int1; // [ int1 ][ int1 ][ int1 ][ int1 ]
long long1; // [ long1 ][ long1 ][ char1 ][ - ]
char char1;
};
class NoMenberVariable { NoMenberVariable() {} }; // クラスサイズはメンバ変数が無いと 1 バイトになる。ただし他のメンバ変数があると +1 バイトされない。何もない時だけ 1 バイトになる。
class Alignment3 {
// 継承関係にあるクラスは virtual 関数があると、インスタンスが親と子のどちらの関数が呼び出しているかを判断する必要がある。
// その継承関係や呼び出しの優先順位を管理したものを 仮想関数テーブル(VTable)という。
// virtual 関数があるクラスは、仮想関数テーブルへのアドレス(つまりポインタと同じで 8 バイトか 4 バイト)が一番初めのメンバ変数としてデータに追加される。
// この並びだと、実態は 23 バイト、アライメントで クラスサイズは 32 バイトになる。
// [ VTab ][ VTab ][ VTab ][ VTab ][ VTab ][ VTab ][ VTab ][ VTab ]
int int1; // [ int1 ][ int1 ][ int1 ][ int1 ][ long1 ][ long1 ][ - ][ - ]
long long1;
long int2*; // [ int2* ][ int2* ][ int2* ][ int2* ][ int2* ][ int2* ][ int2* ][ int2* ]
char char1; // [ char1 ][ nMV ][ - ][ - ][ - ][ - ][ - ][ - ]
NoMenberVariable nMV;
virtual void Func1() {}
virtual void Func2() {} // 追加されるのは仮想関数テーブルへのアドレスなので、virtual 関数は1つ以上だといくつあっても1アドレス分のメモリしか食わない。
};
// ただし、アライメントがどのように行われるのか、どの型がどれくらいの横幅になるのかはコンパイラとかの環境によって結構変わる。例えば std::str がサイズもアライメントの幅も 24 バイトだったり、サイズは 40 バイトあるがアライメントの幅は 8 バイトだったりする。
// そのためアライメントの最適化に躍起になりすぎる必要はないが、大体4の倍数バイトの幅でアライメントが生じてるので、4バイトより小さい型は4以下で間を詰めるようにするべき。
// またパディングのため、ファイルの読み書きなどの、データのサイズからデータの内容にアクセスしようとする時は注意が必要。
// 例えば構造体を介してデータを読み書きする時に、データにパディングが生じていると、データのサイズ分内容にアクセスしようとしてもデータが足りないのでバグる。
struct Data { // 実態は 1 + 4 + 2 の 7バイト。データ全体のサイズとしてはアライメントで 4 x 3 の 12 バイト。
char char1; // [ char1 ][ - ][ - ][ - ]
int int1; // [ int1 ][ int1 ][ int1 ][ int1 ]
char char2[2]; // [ char2 ][ char2 ][ - ][ - ]
};
int main() {
FILE* file;
Data* readData;
file = fopen("読みだす先のバイナリファイル.bin", "rb");
if (file == NULL) {
printf("ファイルオープンに失敗しました。¥n");
exit(1);
}
// fread によるファイルの読み込み時には、パディングされた空白の部分を飛ばして読み込む。
fread(readData, sizeof(Data), 1, file); // 読み込むサイズをデータサイズにすると、Data型のサイズはアライメントされてて 12 バイトだが、内容は 7 バイトしかないので 5 バイト分読み込めずにエラーを生じる。
constexpr int NO_PADDING_DATA_SIZE = 7;
fread(readData, NO_PADDING_DATA_SIZE, 1, file); // これだと上手くいく
}
// あと、データ関連で言うと、バイトを並べる順番(エンディアン)がある
// オブジェクトを表すバイトを並べる順番が処理系によってリトルエンディアンとビッグエンディアンの2つの種類にわかれるのがやっかい。
// この2つの違いについてとかは、なんかデータの中身とか見るようにしてから気を付ければいいのかな?いまだあんま気にする必要性に駆られた場面ない。
///ーー デバッグ ーー///
// ・VisualStudioのデバッグ機能でブレークポイントとステップ実行をすると、特定の変数やメモリの値や動きを追うことができる(コンパイラ言語なのにスクリプト言語みたいに)ので使うのマジ推奨。また使用メモリ容量とかも知れるので、デバッグ効率爆上がりする
// ・通信処理系と並列処理系のバグは、タイミング依存なことが多いので再現と対処が難しい。そのため、その時の状況とログから、理論上の推論で修正する必要がある。例、NULLチェック直後にNULLが入る、、→並列処理で他の箇所でNULL入れてる!たまたまこの処理のタイミングであの処理をした時にしか生じないバグだった、、。
// ・バッファオーバーラン(確保したサイズ以上のメモリにアクセスしようとすること)は、バグのあるコード箇所とクラッシュする箇所が異なるので、バグの追跡が困難になる。確保したメモリ範囲外にアクセスしようとしていないか検知する仕組みを組み込める場合は、組み込むと安全性がかなり上がるのでやるべき。配列扱うときとか型を変えた時に生じやすい。
#pragma region 折り畳み
// 開発環境が VS とかだったらプリプロセッサで折り畳みもできるよ。あんま使わんけど
#pragma endregion
///ーー 最適化 ーー///
// ・(速度)何度も呼ぶ同じ結果を返す関数はローカル変数に格納しておく:例 while (sizeof(array)<0){} → arraySIze = sizeof(array); while (arraySize<0){}
// ・(メモリ, 速度)メモリの容量負荷と処理速度負荷のトレードオフを考えて設計する:例 NG グロ変に大容量の配列おく。
// ・(メモリ)大量にインスタンス化する構造体やクラスのパラメータはなるべく減らす:例 HP と 残りHPと 残りHPの% の3つのメンバ変数を持つ → HP のメンバ変数だけ持って他は計算で導く
// ・(速度)ごく短いメンバ関数はヘッダー内に inlineキーワードを付けて定義も全て記載することで、呼び出しではなく直接アクセス出来て処理が軽くなる。https://qiita.com/agate-pris/items/1b29726935f0b6e75768
// ・(メモリ、速度)論理演算時には、オペランドを短絡評価を意識した順番にする。
// ・(メモリ、速度)値渡しだとコピーごとにメモリ食う&毎回コピコンを挟んで負荷かかるので、参照値を渡すでポインタ渡しや参照渡しにする。
// ・(速度)例外を発生しないことを断定できる場合は関数に noexcept キーワードを付ける。
// ・(速度)関数オブジェクトを使うときは std::functionを使うと重いので避ける。https://qiita.com/suzukiplan/items/e459bf47f6c659acc74d
// ・(メモリ、速度)同じ処理を重複して行う可能性がある場合は、最初の計算結果をキャッシュとして保持して、2回目以降はその値を利用する。
// 仮想関数テーブル VTable を使って、virtual関数と継承によって可能にする動的ポリモーフィズムとは別に、テンプレートを使った静的ポリモーフィズムがある。仮想関数テーブルを使った代表的なデメリットとしては、スタックの圧迫、関数呼び出し処理の増加、インライン展開が不可能になるなどがあり、静的ポリモーフィズムよりもパフォーマンスは低下する。ちゃんと論文とかで VTable が導入されることでプログラム全体の実行時間のうち関数呼び出しが占める時間の割合が明らかに増える、とかがあるらしい。静的ポリモーフィズムについてはまた調べてね。あとここで言及されているテンプレートクラスの特殊化も。https://blog.toylogic.co.jp/programmer/3494.html
// 複数ファイルでの定義を可能にするために inline 使うことは意味あるけど、インライン展開化を目的に inlineをつけても、今はコンパイラが自動で判断するので意味ないらしい。
// 疑問
class A
{
public:
static int count;
static uint8_t arena[];
char ar[]; // 要素数指定しない配列(要素数不明)って書けるっぽい、、、クラスとか構造体のみ?
void* operator new(std::size_t size); // 演算子のオーバーロード
void operator delete(void* p) {}
A()
{
ar[sizeof(A)];
}
virtual void test() {}
A() { printf("ctor %p\n", this); }
virtual ~A() { printf("dtor %p\n", this); }
};
int A::count = 0;
uint8_t A::arena[sizeof(A)* 100];