ここで基礎を学んだので,発展的なことも知っても損害率は20%程度である.知らない方が良いとか,知って損することも多いが.ま,1日目で基礎を学んだら,後は実戦だ実戦! 教科書読んでもすぐ変わっちまうから,読んでも時間の無駄だしな.そろそろC++17が出るので,全ての教科書はゴミになると思われるし, ゆうてるうちにネットの世界はC++20がゴロゴロ出てきている(いやもう世界はC++23ですが). プログラムの世界では「知識」は3年で賞味期限切れだ・・・
というわけで,あくまでも,実戦だ.だから,いつまでもC++第2日なのだ
- 関数引数の配列
- キャスト(型変換)
- コンテナ(配列強化型スペシャル的なやつ)
- 複素数値
- メンバー編集初期化リスト
- 任意精度計算
- バイナリファイル
- オーバーロード(関数の多重定義)
- テンプレート(型に嵌ったクラス)
- 関数ポインタ
- ラムダ式
- 可変長引数
- 線形代数および微積分
- スマートポインタ
- const
- 右辺値参照&&
- 継承
- ポリモーフィズム
- 合成
古典的黒魔術
古いプログラムを見ると, 奇怪な構文が見られる. 代表例を示す.
- printf("%.5lf %d\n",.....)
これは, 古生代に用いられた,数値を出力する方法です.ハナモゲラ文字を用いて,数値の桁数を指定したりします.変態ですね.無視したいところですが,別のプログラム言語では現役なので,結局覚える羽目に陥るでしょう. - double X[200];
実はこれも配列です.ですが,途中で要素数を変更するのは不可能です.不思議なことに, Xから要素数を知る手段が存在しません.まあ,範囲をはみ出して利用してもエラーは起こらないので,プログラムが停止することはありません(結果は間違いです).std::vectorに標準装備の,データの一部切り離しとか,混合とか挿入とかは一切できません.速度的には, std::vector で[]を用いた場合と同程度です.これを利用する利点は, ありません(メモリー使用量が配列1個あたり8byte小さいかも).まあでも手軽なので,間違えようがない場合などで使われることはあります. - char filename[128];
文字の配列です.文字列ではありません.しかし,std::string がない頃には,文字列の代わりに用いられました.残念ながら文字列の長さなどが定義されていないので,char(0) NULLが起こるまでが文字列の長さだ,などとローカル・ルールでなんとかします.途中でNULLを含む文字列は・・・そんなことを考える悪い子のところには,もれなくブーギーマンがやってきます.なにぶん単なる配列ですので,文字列を結合したり,分解したり,コピーしたりが,むやみと複雑怪奇で理解不能と言えるでしょう.文字列として日本語が入ると,もはや神の領域です.この例では文字数が128文字になってますが,128文字以上を書き込んでも止まらないので,セキュリティーホールってゆうんですよね〜(バッファーオーバーフロー,押し入り強盗ともいう) - malloc(...), double X[];
先ほど double X[200]; の要素数が変えられないと言いましたが,魔導師ならばmalloc呪文により要素数を変更できるのです.ただし, デストラクタでfree呪文を唱える必要性を認知しない似非魔導師も多く,memoryリークにより終局(システムクラッシュ)を導くことが多くあります.まあでも,C++でも new して delete しない奴は,同類なんですけどね.自分がバカな場合には,コンピュータが賢ければ問題はありません.スマートポインタを使いましょう.std::vectorでemplaceした場合には, deleteは自動ですので,あなたがバカでもOKです. - fopen("filename",....)
std::fstream が発生していない時代のファイルに出力する方法です.fprintf(ハナモゲラ)を用いて出力を生成します.書式をつけた美しい出力を得るには, ハナモゲラと格闘せねばならず, バイナリーファイルを出力するとVTKよりも巨大なファイルサイズになってしまいます.XMLの入出力は諦めた方が良いですね.まあ,利点はじい様が歓ぶくらいですな. - struct CC {....}
C++言語には構造体というものがあって, データと手続きをセットに記述できる. それはクラスと同じではないか?というと,同じである. 違うところは, クラスデータはヒープ領域に確保されるが,構造体データはスタック領域に確保される.ヒープ領域とスタック領域の違いは,まあ東京で言えば
ヒープ:埼玉県 スタック:山手線の中
わかりますよね.山手線の中は便利で何事も早いですが,すでに満杯ですので,不用意に使えば,メモリーてんこ盛りのPCでも,すぐにメモリ不足になります.埼玉県ではメモリーは有り余っているので,いくら使っても無くなりません.が,所詮,埼玉であることは覚悟してくださいな
関数引数に配列を
関数の引数に配列を与える方法は,
void method(std::vector<double> AA); (1) void method(std::vector<double>& AA); (2)
(1)では, 関数呼び出し時にAA配列のコピーが作成され,関数終了時に破棄される. 関数内部でAAを変更しても,呼び出し側の配列の値は不変である. (2)ではAA配列の参照が引き渡される.関数内部でAAを破壊すると,呼び出し側の配列も破壊される.というわけで,計算速度は(2)の方がコピーを作成しない分, 早い(←最近のコンパイラは,これくらいのことはお見通しで最適化してしまい,ちょっとしたサンプルでは計算速度が変わらないかも.あくまで「原理的には」速度が違い得る,ってことで).
黒魔術的配列を引き渡す場合,
void method(double AA[]); (1a) void method(double *AA); (1b) void method(double (&AA)[5]); (2a) void method(double AA[5]); (2b)
(1a)では, 関数呼び出し時にAA配列を渡しているような気分になる.しかし実際には, AA[0]しか引き渡されない.具体的には(1a)と(1b)は同じものである.
なんでそんなもので動作するのか?というと,A[i]とは, A[0]からi個進んだメモリーの値である,と定義されているからである.
(1a)(1b)いずれの場合も, 配列のコピーは作成されない.
黒魔術配列では,配列から要素数を知る手段が無いので, (1a)(1b)の書き方はバグの温床であり,見つけ次第,殲滅するべきものである.その代わりに用いられるのが, (2a)である.これは参照を渡すのであるが,そのとき,配列の要素数が5でなければエラーとなる.あ?(2b)はAA配列の5番目のアドレスをmethodに渡しているので・・・そもそも配列を渡しているとはいえない. もちろん, AA[5]のアドレスの前後を強引にアクセスすることは可能だが・・・それはやめよう,バグの素だから.
キャスト演算子
プログラム中で, 変数の型を変換する(整数3を,浮動小数点数3.0に変換するとか)局面は多いです.これを「キャスト」と呼びます.その書き方はいくつか提供されています.
数値から文字列への変換(浮動小数点変数Xの値を"2.354e+3"とかの文字列にする,あるいはその逆)は「キャスト」ではなく,「書式つけ(フォーマット)」でして,ここでは述べません.
古典的キャスト
丸括弧の中に変数型を入れると, その後ろの値を指定した型に変換します.
(double) x
ひどく安直ですので,コンパイラーの文法チェックを突破するのに便利です.んまあ変数一個なら大丈夫だが, xがクラスやら派生型のポインターだったりすると,だんだん怪しげになってきて,変換したクラスでメソッドを呼び出したときに何が呼ばれるのか?とかが分かりにくくなってきます.というわけで,実行時にどうなるのかは,貴方の力量次第です.
斬新なキャスト
運を天に任せ自分を信じてキャストするのが心配な場合,文法チェックが可能な新しいキャスト方法が用意されています.
static_cast<T>(v)
変数vを, 型Tに変換します. 後の多態(ポリモーフィズム)で利用するように,vがポインターの場合でも,なんの解釈もせず言われた通りにTに変換してしまい, 派生クラスで追加した機能や数値が落ちます.
dynamic_cast<T>(v)
実行中のプログラムで,変数vの実際のデータに応じて型Tに変換します.多態(ポリモーフィズム)では, 派生クラスTのオブジェクトを基底クラスのポインターvが示している場合, dynamic_castは正しい派生クラスへのポインターに変換します. 当然ですが, static_cast<T>(v)よりも計算速度は爆遅です.
reinterpret_cast<T>(v)
なにこれ?
const_cast<T>(v)
なにこれ?
STLコンテナ
ここまでstd::vectorを配列と呼んできたが, データを保存する構造は他にもたくさんある.よく使うものを紹介しよう.
メモリーが連続に配置されてほしい場合
これはstd::vectorしかありません(←固定長ならstd::arrayがあるやんけwww). 連続に配置されているので
- ランダムアクセスが得意. すなわち value[i] でi番目の要素に読み書きできる
- 運が良ければキャッシュに入り,高速計算
- サイズを変えたり, 途中に要素を操縦したり削除すると, 「連続メモリ」制限のため,内部で全部の要素を作り直してしまう.
- だから, 要素にポインターでアクセスしているとヤバイ.これは確実にヤバい. 要素が削除されたり追加されたりすると, そのポインターは廃棄されたデータ領域を指差してしまう.
- 富士通だったかのコンパイラは, std::vectorのスタック行きのやつ(ローカル変数)があると並列化しないと聞いた. std::arrayだと大丈夫だそうだ
利用方法はこちら
要素の削除・挿入が頻繁な場合
std::deque が良いのではないでしょうか. これは連続メモリーを諦め, 微妙に機能拡張したstd::vectorです.
- 先頭と末尾への高速な挿入
- 中間での低速な挿入・削除
- 先頭と末尾ならば削除・挿入しても他の要素の移動やコピーは起こらないのでポインターも安全.
- ランダムアクセスが可能. すなわち value[i] でi番目の要素に読み書きができる
- サイズを変えると,追加分を別の場所に確保する
をサポートしています.利用方法はこちら
途中要素の削除・挿入が頻繁な場合
std::forward_list もあります.これはstd::vectorとはかなり違います.
- リストのどの位置でも高速な挿入と削除が可能
- 削除・挿入しても他の要素の移動やコピーは起こらない
- ランダムアクセスは不可能
- 次の要素, 次の要素,...と芋づる式に手繰ることができる
です. 利用方法はこちら.
要素の前後を芋づる式にたどりたい場合
std::list を使いましょう. 基本は std::forward_list と同じですが, リストを前の要素に手繰ることも可能になっています. 要素のダイレクトアクセスが不可能で, 追加と削除が面倒です.
キーで検索したい
std::map は, キーと値のペアの集合を保管します.
- std::map は, 写像ですから多価ではありません. つまり, あるキーに複数の値が存在することはありません.
- キーを与えると, 値を得ることができます.
- キーの集合や, 値の集合を得ることができます.
これに対し, std::set, std::unordered_set, std::multiset はキーの集合を保管します. サンプルはこちら
- std::set は, 順序集合を表します.集合なのですから, 同一の値を複数保管することはできません.
- std::unordered_set は順序が存在しない集合を取り扱います. 順序をソートしない分, std::set よりも大幅に高速化されています.
- std::multiset も, 順序集合を表します.集合なのですから, 同一の値を複数保管することができます.
先入れ先出しの箱が良い場合
std::queue は, 要素を追加することと, 要素を取り出すことが可能です. 先に入れた要素から順に取り出されます. つまり, 1, 2, 3, 4 の順に要素を追加したら, 取り出した時 1, 2, 3, 4と出てきます.基本的に要素の追加と取り出し以外の機能はありません.
先入れ後出しの箱が良い場合
std::stack も, 要素を追加することと, 要素を取り出すことが可能です. 後に入れた要素から順に取り出されます. つまり, 1, 2, 3, 4 の順に要素を追加したら, 取り出した時 4, 3, 2, 1と出てきます.基本的に要素の追加と取り出し以外の機能はありません.
バイナリーファイル
テキストファイルではなく,2進法のメモリーイメージ(バイナリーデータ)をファイルに書くこともできる. 各種のデータ圧縮ライブラリーには劣るが,テキストよりは多少小さなファイルサイズが可能である. fstreamのコンストラクタでstd::ios::binaryを与えればよい:
std::fstream fp(filename,std::ios::binary);
その他のキーワードはGoogle chatGPTにお願いしてね. std::ios::app, std::ios::trunc, std::fstream::read, std::fstream::tellp, std::fstream::seekpくらいかしら?
複素数
複素数はstd::complex<double>ですよ. いける関数はabs(), arg(), polar(), real(), imag(), conj()っすね.
メンバー変数初期化リスト
クラスを初期化する際に, メンバー変数の初期化に引数を与えなければならなかったとしましょう. 例えば
class myClass { hisClass He; public: };
ですが, hisClassのコンストラクタが, パラメータ hisParameter Xを必要とする,という場合です.この場合, Heを初期化できるパラメータXを, myClassにもつけて, メンバー変数初期化リストでHeを初期化しなければなりません.
class myClass { hisClass He; public: myClass(hisParameter& X):He(X) { }; };
クラス以外でも, メンバー変数が参照であれば, メンバー変数初期化リスト初期化しなければなりません:
class myClass { hisParameter& X; hisClass He; public: myClass(hisParameter& X):He(X),X(X) { X.update(); this->X.update();
}; };
さてここで問題です. myClassのコンストラクタで, XをX.update()で変更してしまっています! 変更されるのは, 緑色のXなのか?紫色のXなのか? また, 初期化で一体どんな値が設定されるのか?答えは
- C++は,インスタンスを作成するときに,プログラムに書いてある順番に変数を作っていきます.
- ので, 最初に発生するのは,この紫色Xの初期化です.で,それは初期化リストにX(X)と指定されているので, 緑Xの参照が紫Xに設定されます.
- 次にhisClass Heの初期化に取り組みます.で,このときには緑Xを用いてHeを初期化します.
- 変数が作成できたので, コンストラクタを実行します.myClassにはメンバー変数Xが定義されてはいますが, それは引数のXで隠れてしまっているので,X.update()でupdate()されるのは, 引数のXです!
- 最後にthis->X.update()を実行しますが, これはthisつまり現在のインスタンスのメンバー変数Xをupdate()します.とはいえ,このプログラムでは, メンバー変数Xは引数Xへの参照ですから,結局は, Heの初期化を最初のXで行ったのち,Xのupdate()を2連続で行う,というプログラムになります.
オーバーロード
古典的プログラム言語では,関数の名前を一つ定めれば,(少なくともユーザーは)その引数の型や順序を一通りにしか定めることができなかった.しかし,関数の名前は作業を表すものであるから,作業対象である引数が異なる多数の関数が必要になる場合もある.例えば, 2つの引数の大きい方を与える関数 madMax(x,y)を定義する場合,x,yが整数の場合,実数の場合....の多数の関数を同じ名前で作成したくなる.これはC++ではオーバーロードと呼ばれ,許容されている.コンパイラ〜は,引数の型から呼び出すべき関数を決定し実行する. 具体的には
unsigned int Calc(unsigned int x){....} double Calc(double x, double y){....}
このように記述できる.呼び出すときに z=Calc(p); と書けば第一の定義が, z=Calc(q,r)と書けば第二の定義が採用される.
テンプレ
作業内容は同一なのであるが, 変数の型だけが異なるクラスや関数が必要となるケースは多い.圧力の微分を定義したら,流速も微分したくなった・・・ん?圧力はスカラー量だが,流速はよく考えたらベクトル量だった,とか.それを解決する書き方が「クラス・テンプレート」である.まず「テンプレート」というクラス・関数の材料をヘッダーファイルに書いておく.クラスの場合
template <typename T>class Vector2 { private: T components[2]; }
この例では,<typename T>
class Vector2<double> myVector;
これで, myVectorオブジェクトが完成します.その定義は, Vector2 テンプレートで T=double に選んだものである.
クラス・テンプレートはクラスではありません.Tを指定するとクラスになるテンプレートです.だから,.CPPファイルに書いてはいけません.クラスを生み出していないテンプレートに文法ミスがあっても,コンパイラは発見できません.これを「テンプレートクラス」と呼称すると間違いの元です.
void *タイプ
関数の引数をいちいち分類するのが面倒くさい場合,どんな引数でもOKという,超テキトーな書き方も可能です.関数の引数でのみ可能な, void *型です. こいつは,何か不明な変数へのポインターを引き渡すことが可能です.ま,渡す方は,渡したいデータのポインターを入れるだけですので簡単ですが,渡される方では,どんなデータへのポインターなのかを明記する必要があります:
void MyFunc(int id, void* parameter){ switch (id) { case (0): Paramter_Class& P=*(Paramter_Class*) paramter;//貴方は型を知っているのでキャスト ・・・Pを使って記述・・・ (もちろん上のキャストが間違ってたら,運が良ければSegmentation Faultで止まってくれる) break; default: } }
関数ポインタとstd::function
どんなポインタも引数にできるので, 関数を引数とする汎関数を定義できます.
旧来の書き方
まず, 特定の引数ー例えばdoubleとvoid*のペアーを持つ, doubleの値となる関数を考えましょう.関数の引数として表現するには, 次のように記述します:
some_function(other arguments,..., double (*f)(double,void*),....){ .... double z=(*f)(x,parm);// 実際に関数の値を得るには, (*f)が関数なので (*f) を呼び出す .... }
関数名は「f」で,定義文の内部に入り込んでいることに注意してください. また, 関数がポインター *f として定義されていることに注意しましょう. 関数そのものは別のどこかに実体があり, some_function()は, 引数として, その関数オブジェクトを指差すポインターを受け取る方針なのです.
int型の変数IJKを定義する場合,変数が行末: int IJK;
しかし関数FGHの場合,: double (*FGH)(引数リスト.......引数リスト):
ですので,変数として定義する場合でも,同じ流儀です:
double (*my_func)(double,void*); // 関数(double,void*)-->double void (*his_func)(void); // 関数(void)-->void int* (*her_func)(void); // 関数(void)-->int*
この行で定義する関数の名前my_funcがずいぶん前の方にくるので, わけわからんと思う人は多いですが, やむを得ませんね. こうやって定義いた「関数ポインター」には, どこか別の場所で定義した関数へのポインターを代入できます:
double ACT_Func(double KK, void* P){ ....なにか本当の関数.... } .... void my_process(...){ double (*my_func)(double,void*); // 関数(double,void*)-->double my_func=&ACT_Func; .... }
ここで, グローバルな関数 ACT_Func()をポインターに代入することに注意してください. クラスのメンバー関数は, 実は「隠れ引数」を持っており, std::bind を利用しなければ関数ポインターには代入できません.
ということは, 関数ポインターを使えば使うほど, たくさんのグローバル関数が必要になり, その名前が重複しないように定義していくのが, とても大変な作業になります. この問題への解決は, のちにラムダ式で説明します.
新しい書き方
まず定義文が醜くて困る,ということで,新しい書き方が用意されました:
std::function<doble(double,void*)> my_func;
ひどくわかりやすいので, 説明は不要ですね.
ラムダ式(無名関数)
ラムダ式とは, 名前がなく, 呼び出すことが不可能な関数です.そんなものが必要になる理由は
- いま,ここだけでチョビッと関数みたいな感じで使った方が記述が楽
- コールバック関数など,クソほどたくさんある関数を定義したいので,重複しない名前を考えるのがクソ面倒
といったところです.そもそも「手っ取り早く関数ちっくなものを定義したい」わけですので,非常に簡単に定義できます:
auto F=[](){std::out<<"fat ass!" << std::endl;};
これで引数なしの関数を定義したことになります. 最初の[]がラムダ関数の開始命令で, その返り値が「名前の無い関数オブジェクト」です. これはどう使うのかといえば,
F();
これで呼び出すことができます.Fは関数名ではありません,関数の内容を保存している「変数」です.だから, PoiPoiという変数にコピーしたら,同じ関数の呼び出しが PoiPoi() になります.なんといっても変数ですから,他の関数の引数に与えることができます.
some_function(other values,....., [](){ ..define your function here.. }, ....);
この例では,もう変数に代入するのも面倒なので,関数呼び出しの引数リストの一つとして関数の値(関数で行う計算内容)を書いてしまっています.ふっ,と関数が定義できるので,コールバックであるとか,被積分関数であるとかを記述するのが簡単です.
関数の値がどんな型であるのかは, returnするデータの型で定まります. 関数に引数がある場合には, ()の中に記述します:
auto F=[](double x,void *p){double *P=(double *)p; return (double)(*P+x);};
さて, ラムダ式はプログラム中に「ふっ」と現れるわけですので, その外部の変数を使いたいという欲望は当然です. これは, []の中にキャプチャを指定することで可能になります. ただし, キャプチャを有する関数は可搬性を失っているので, 関数を引数とする関数に用いるのが(「周囲」の環境が不明になるため)困難です.
参照キャプチャ
次のように[&]と宣言します
double S=50; auto F=[&](double x,void *p){double *P=(double *)p; return (double)(*P+x+S);};
外部の変数Sを利用することができます. &では, スコープの全ての変数が参照として引用できます. もちろん,関数内部でその値を変更すると,外部の変数の値も変わってしまいます.
- 外部の変数の参照をゲットする,というところが微妙で, ちょっと前のGCCでは外部の変数や参照の参照が利用できました.
- CLANGは, 外部の変数の参照は利用できましたが,外部で定義されている参照は変数ではないので,利用できませんとエラーするようになりました.
- CLANGが正しいらしいです・・・言葉は正確に!ということらしいんですが
コピーキャプチャ
[変数名,変数名,...]と定義すると, リストした変数のコピーが作成され, 関数内部で利用できます.悪魔でコピーですので, 大きなデータのコピーは時間がかかり, また, 関数内部で値を変更しても関数の終了時に破棄されます. コピーして便利な変数には this があります. こいつは, ラムダ式を定義しようとしているインスタンスへのポインターですが, ただの整数値アドレスですので, コピーキャプチャしても何も問題はない. で,ラムダ関数内部では this-> を使って,外部環境にアクセスできるわけですね! ・・・でも,ラムダ式はあくまで変数なので,
class myCLASS {....};
myCLASS myOBJ_1; ← コンストラクタでラムダ式を構成したとする
auto myOBJ_2=myOBJ_1; ← コンストラクタが走らないので, myOBJ_2のラムダ式はmyOBJ_1のコピー
こうやってインスタンスをコピーすると, ラムダ式はただの値なので,myOBJ_2のラムダ式のthisや参照はmyOBJ_1を指差したままなので注意! myOBJ_2を指すようにするには, ラムダ式自身をリセットする必要がある.
ラムダ式で参照を返す
ラムダ式の戻り値は自動で決まると申し上げましたが, 実はデフォルトでそうなっているだけです. デフォルトを正確に書くと
auto F=[&](int ival)->auto{....; return some_value;}
autoでsome_valueの値を推定して戻り値にしています. これを利用して, some_valueの参照を返すことができます:
auto F=[&](int ival)->decltype(auto){....; return some_value;}
こうしておけば, 関数の戻り値に代入することだって可能です:
auto F=[&](int ival)->decltype(auto){....; return some_value;}
F(12)=13.22;
これで, some_valueに13.22を代入することが可能です. もちろん, some_valueが外部の変数である場合に限り,意味があります.そうでなければ, 一時変数に値を代入して,その後すぐに消えてしまいます.
std::bind
ラムダ式やstd::functionを用いると, 関数を引数にして処理できるようになりました.すると,「定義した関数と,引き渡し先で必要な関数の引数が一致しない」というケースが出てきます.これは拘束プレイを行うstd::bind で解決できます.
クラスのメンバー関数を引き渡す
クラスのメンバー関数を,別の場所で使うには
class MyClass {... public: double Method(int i,int j){....} }; MyClass obj;
MyObjectのMethod()関数で,第二引数を3にした関数Method(*,3)を引き渡すには,
auto F=std::bind(&MyClass::Method, &obj,std::placeholders::_1,3);
と定義すれば良いです.&objはクラスのアドレスですから,同じクラスのメンバー関数の中では this でじゅうぶんです.Metodがオーバーロードされている場合には, 「おい,どいつを拘束するんだ?」とエラーしますので,
auto F=std::bind<double(MyClass::*)(int,int)>(&MyClass::Method, &obj,std::placeholders::_1,3);
としましょう.
グローバル関数を引き渡す
グローバル関数(キャプチャなしのラムダ)は簡単です.
エラー検出関数
プログラムがバグってると, 数値がNot_a_Number(NaN)になったりInfinityになったりします.プログラムでそれを調べるには,:
- std::isnan(value) value=NaNでtrueになる
- std::isinf(value) value=±infinityでtrueになる
- std::isnormal(value) valueが0に近すぎてアンダーフローすればfalseになる
- std::isfinite(value) value=±infinity or value=NaN でなければtrue
さて, NaNは見たくない数値かもしれませんが,
- 数値以外に, ON/OFFの値を設定できる
という点では非常に便利です.数値か, あるいは「未設定である」ということを判別できるようになるからです.具体的には
#include <limits> //numeric_limits #include <cmath> //isnan ... long nt; nt=std::numeric_limits<long>::quiet_NaN(); std::cout << "nt= " << nt << " isNaN= " << std::isnan(nt) << " for has_quiet_nan= " << std::numeric_limits<long>::has_quiet_NaN << std::endl; double dx; dx=std::numeric_limits<double>::quiet_NaN(); std::cout << "dx= " << dx << " isNaN= " << std::isnan(dx) << " for has_quiet_nan= " << std::numeric_limits<double>::has_quiet_NaN << std::endl;
こんな感じに使います. 整数はquiet_NaNが存在しないので, 青字部分ではただの0が代入されてしまいます:
nt= 0 isNaN= 0 for has_quiet_nan= 0
しかしdoubleは, 赤字部分でNaNが設定されるので, 数値以外に「NaNである」「NaNではない」を識別可能です:
dx= nan isNaN= 1 for has_quiet_nan= 1
NaNはファイルに保存したりロードできるのかというと, できます. ごく普通に, IEEE754規格で定義されているからです.
可変長引数
関数の引数が場合によって変わる時, f(x), f(x,y), f(x,y,z),....と別々に書いていくと, 面倒だしバグが増えます.そんな時には, 可変長引数の仕組みを利用しましょう.
初期化リスト方式
初期化リストとは{値, 変数, ....} という感じの,チュウ括弧で括られたものです. std::initialize_list<typename> みたいな感じで使います. 具体的には
... int some_func(int A, double B, std::initializer_list<int> list) { .... std::cout << "おまいの引数は" << A << " と " << B << " と, 他に " << list.size() << "だ" << std::endl;
これで, 任意個数の整数がゲットできます.いやまだ数しか分かってねえか.個別の要素にはboost::tokenizerみたいにイテレータでアクセスする以外の方法がないです:
auto ptr=list.begin(); int i_value=*ptr++; ...
このsome_funcを呼び出すには,
...=some_func(1,5.2,{2,3,4,5});
みたいに{}つけて呼ぶので,人によっては「だっせぇ」と思うかも.
可変引数テンプレート方式
例えば, あるクラスのメンバー関数を
template <typename... Rest> int some_func(Rest... rest)
と宣言したとしましょう. すると可変長の型名 「Rest...」 が使えるようになります. が, この可変長型名, なかなかうまく使えません. できることは
- 可変長変数 rest の中に, いくつ引数があるのかを, sizeof...(rest) で調べる.
- std::initializer_list<T>に変換する
auto myarg=std::initializer_list<int>{rest...};
myargは初期化リストと同様に処理できます.だが,これでは変数が全部 int になって困る場合,再帰テンプレートみたいなのを工夫して作るらしいです. つまり
template <typename... Rest> int some_func(int fist, Rest... rest)
を定義しておくと, int first が減った関数を呼び出そうとする. で,対応するものがないと, 小さくなったリストで some_func()を呼び出す. 最後の一個を
int some_func(int last)
で定義しておけば良い. これを使って工夫するそうですよ. 例えばこれはstd::vector<T>を継承じゃなくて移譲するクラスを作るときに
templace <class... Args> myType& emplace_back(Args&&... args) { return mydata.emplace_back(args...);};
とかで,内部のstd::vector<T> mydata に任せちゃうなんて時にも,書かなければなりませんですね.
BOOST, EIGENおよびGSLついでにCGAL
基本的な作業は,あなたが腐ったようなプログラムを書くよりは,プロが書いたライブラリーを利用したほうが高速で精度がよいです. BOOSTはファイルやフォルダ, 並列計算やマルチスレッドなどを含む汎用のライブラリです.行列を取り扱うのはEigenですね.GSLは数値計算にありがちな,数値微積分,線形代数,補完や特殊関数をBOOSTを用いて記述したライブラリーです.CGALは幾何形状を取り扱う関数をBOOSTを用いて記述したライブラリーです.
利用可能にする方法はこちらです.
- Boostを使うと,みんな大好き多次元配列を作ることができます.
- Boostはファイルやフォルダーの作成削除が得意です
- Boostは,コンピュータのコマンドを実行したりできます.
- Boostは,並列計算が得意です.
- Boostは,任意精度計算が可能です.
- CPU時間を計測も精密です
- Boostでも乱数を発生させることができます.
- Boostはデータの並び替えが簡単です.
- Boostは補間も得意
- Boostは常微分方程式を解く
- Boostに存在しない特殊関数はないのでは?
- Boostは,実世界日時にアクセスできます
- Boostを用いて,ネットワークを使いましょう
- BoostはParseもできるらしいぞ
EIGEN
- 密行列を取り扱う
- 疎行列を取り扱う
GSL
- マニュアル
- 特殊関数を使う
- 方程式 f(x)=0を解く
- 固有値を求める
- 統計処理を行う
- 数値積分
- ソートする
- 常微分方程式を解く
- スプライン補完
- LU分解や固有値算出
- 乱数を発生
CGAL
newおよびdelete
ここま
スマートポインタ
ポインターではNULLになっているかもしれないので,できるだけ参照にしようとして
class A; //クラスAを前方宣言
class B {
public:
A* ptr_A; //前方宣言したクラスのポインタ
A obj_A; //前方宣言したクラスの実態
}
と書くと, 実態のところでエラーする.前方宣言では名前しかわからないので,ポインターは作れるが,実際にそこでメモリーを確保して変数を用意することまではサポートできないのである(Javaではエラーしないらしいが. Javaは仕様上全部の変数が動的確保なので,参照の実態はC++のポインターに近いものである,というだけらしい).
じゃあ, ポインターにしようっと.でも消し忘れるよな俺って.←メモリーリーク野郎で漏れまくりだからな
そんな貴方に,スマートポインター! class Bを作って確保したポ ptr_A の指の先のメモリーは, class Bの消失とともに自動的に消えるというありがたい代物です.バカにぴったりですね!
- std::unique_ptr<T> あなたが使え,貴方だけが使えるTを準備します.貴方が死ぬとTも死にます.
- これを使うと,貴方をコピーすることができなくなります.「貴方だけ」の変数が含まれているのに,貴方のコピーが可能だと,使える人が2人になって,変だからです.
- それでもコピーをしたいなら,お前のクラスにコピーコンストラクタを書くのだ!
- 例えばclass Bを std::vectorにpush_back()できなくなります.こいつは「引数のコピーを配列に追加する」命令だからです.emplace_back()してください.
- 関数の引数にunique_ptrを与えることは不可.「あなただけが使える」に反してしまいますからね.
- 関数の引数にunique_ptrが確保したデータの参照を使うのはOK
- 確保したデータの所有権をstd::moveで他の人に与えることが可能です.所有権を譲った後は,あなたは他人です.いくら昔話をしても,よりが戻ることはありません.
- std::release()で,データを身請けすることが可能です.得られたデータはstd::unique_ptrの管理外ですので,自動的に消える能力を失っています.
簡単な例: クラスTakoのオブジェクトを作成する std::unique_ptr<Tako> object_Tako; この状態では object_Tako=nullptr; になっている object_Tako.reset(); ここでobject_Takoオブジェクトが発生する. object_Tako.reset(new Tako(ika)); Takoクラスのコンストラクタに引数ikaが必要な場合
- std::shared_ptr<T> せっかくGetしたTを独り占めとは,そりゃ幾ら何でもけち臭い, みんなで楽しもうや,という場合には std::shared_ptr<T>を使ってください.
- こいつは相手する人の数を指折り数えています.相手が誰もいなくなったら自死します.
- というわけで, カウンターとTをセットで作成しなければなりません.面倒なので,make_shared()を用います.
- 指折り数える分,遅いです.
- shared_ptrしたTの内部に自己へのポインターがあり,お互いを指差していると, 相手をしている人の数が絶対に0にならず,結局はメモリーリークと同じことになります.
- std::weak_ptr<T> メモリーリークしないshared_ptr()です.
- 所有権なし,参照のみ可能です.
- 参照している間に所有者がいらんこと(free)しないようにlock()してから参照します.
- lock()していない間に所有者がfreeしてしまい,参照先がnullptrになることもあります.
- となると,もう,旧来のポインターとあまり変わらん気がします. ま,expired()によって,所有者が所有を放棄したかどうかを知ることができる,だけですな.
const
定数であることを宣言できます.もちろん宣言しなくても良いのですが,定数であることを宣言すると,
- 計算が早くなる.かなり早くなる
- 変更してはならぬ変数を変更する系のバグが発見しやすくなる
という利点があります.const宣言が使える局面は
const変数
変数が不変だというわけで,「定数」です. 次のように使います
const double MAX_VAL=12.0;
これは簡単.MAX_VALという定数を宣言するだけです.
const引数
関数の引数にconstを宣言すると, 関数内部でその変数を書き換えるバグを防げます.
void MyMy(const double MAX_VAL){.....};
関数内でMAX_VALを書き換えようとするとエラーしてもらえます.
void MyMy(const double* MAX_VAL){.....};
関数内でMAX_VALの指差す先を書き換えようとするとエラーしてもらえます.
constメンバー関数
constなオブジェクトの関数を呼び出す場合, 「その関数,呼び出したらメンバー変数書き換えるんじゃなかと?」と心配になるので,コンパイルエラーが起こります.そこで, 「俺っちはメンバー変数を書き換えない,いい子ちゃんだよ」と宣言することができます.
void MyClass::MyFunction (double* MAX_VAL) const {......};
良い子であると宣言することは,実際に良い子であることを保証しません.(最近コンパイラのチェックが厳しくて, constメンバー関数内部でメンバーを書き換えるのが本当に困難ですね.)
右辺値参照&&
んー,なんか変だ. やっぱstd::moveが先か. なんかポイント外してる. 使い所間違っている気がする. コピーを許容するなら普通の参照でいいからなあ. やっぱ交換するとか,そういう例しかないのか? なんか違うんだが.
次のプログラムを考えよう.
double x; x=1.23
右辺の1.23はこの行を実行しているときには計算機に存在するが,次の瞬間には霞のように消える「値」である.これを「右辺値」とよぶ.左辺のxは, 名前のついたオブジェクトであり,値ではない.これを「左辺値」とよぶ.通常の参照 & は, 左辺値を参照する. 例えばMyClass()インスタンスが2個必要であったとしよう.
MyClass x=MyClass(); (1)
MyClass y=MyClass(); (2)
MyClass& z=x; (2-FAIL)
MyClass& w=MyClass(); (2-ALT) ううん,なんかもうちょっといい例はないのかな
(1)では, MyClass()がコンストラクトされたのち, xにコピーされる. コンストラクトが30分, コピーが1分かかるならば, 31分が必要である(本当か?NRVOでなんかされるのではないのか?確か, 代入するのが明確ならコピー省略してそのまま構築するので30分のような気がする). (2)でも同じ時間が必要である. (2FAIL)は作成済みの左辺値x を, y と名づけただけで, 時間はかからない. MyClassのオブジェクトが2個必要なら, これではうまくいかないことになる. (2-ALT)では30分で無名データをコンストラクトし, それをzと名付けているので30分かかる.
別な方法は, MyClass()のコンストラクタの結果を保存しておき, xとyにコピー代入することである.
MyClass&& x_ref=MyClass();
auto x=x_ref;
auto y=x_ref;
&&で宣言された変数は,右辺の値の方(本来,霞となって消えるべきMyClass()の結果)を参照する. 従って, 同一の結果をxとyにコピー代入することができるので30+2=32分で済む.
いやそれが目的ならMyClass& でも同じなのでは.
継承
プログラムを書いていると, この部分と, あの部分は, ある機能を共有しているということが頻繁に起こります.例えば
- 数値解析プログラムを書いていました. あるクラスAと, あるクラスBは, どちらも「時間 time 」という値を持たねばならぬことに気づきました.
- 基本の計算式を矩形領域で実現するクラスを作成しました.ところが, 右側が無限遠方であるクラス, 上側が無限遠方であるクラスも必要になりました.ほとんど同じなんですが.
まあようするに, 微妙に機能が異なるプログラムを作りたい場合が, 多くあるということなのです.例えば,トンボというクラスと,チョウ,ハチ・・・いうクラス,という感じです.微妙に違いますが,全て飛行する昆虫であります.ですので,flap_down() flap_up() とか似たような関数で操作したいのですが,実際の内容が同じ場合,異なる場合がそれぞれある.こんなときには継承機能を使います.
- 太古Fortranとかでは,全部コピペで作ってました. 基本ケースを一個作り,コピペして64通り作成とか.しばらく使っていると,21番目で訂正があり,それをちゃんと18番目と43番目にも反映させて安心していたら,じつは49番目にも必要なことを忘れて・・・えらいことになった,とか,よくやってましたwww
- コピペを避ける手法としてテンプレート(型に嵌ったクラス)もあります. でもこれ,見にくいんですよね.まあ書いてみて適した方を使えば良い
これは,飛行虫クラスという共通機能のクラスを作り,それを拡張してトンボ,チョウ, etc. を作成する.
- 共通部分は共通機能クラスの1箇所にしか書いてないので,そこでの訂正は忘れずに適用される.
- 特殊ケースの修正が共通機能を破壊することも少ない.
- すると,解析上のある一つの操作に対応するプログラムは,ここ一箇所にしか書いていない,となっていきます.すると,解析式とプログラムの対応がつけやすくなる.
上の「時間」を共有するケースでは, 「時間を持つひと」というクラスを作成し,それを拡張してクラスA, Bにするわけです.
基底クラス
元になる飛行虫クラスを作成します.これは「全てのクラスが共通で持つもの」を定義します.これを基底クラスと呼びます.トンボクラスなどは,飛行虫クラスを拡張して作ります.それを派生クラスと呼びます.具体的には
class Param //基底クラスのサンプル. Paramは, 「時間 time を持っているひと」である
{
public:
double time;
};
派生クラス
「時間 time を持っている別のひと」派生クラスを作るには次のように書きます:
class Region : public Param
{
public:
double dx;
}
さて, Regionは「時間 timeを持っているひと」であるので, こんなことができます:
Region myR1;
myR1.time=0.5;
Region myR2;
myR2.time=0.3;
このとき, myR1は, Regionクラスですが, 同時に, Param classです. ですから,Regionの定義にtimeがなくても, 「基底クラス」Parmに定義されているので,利用できるわけです.これを"Region is a Param"と表現します. RegionクラスはParamクラスを継承していますが, それは単に「RegionはParamである」だけであり, 「myR1とmyR2は,単一のParmインスタンスを共有している」とは言っていません.だから,こことここで設定している値の間には関係はありません.
もし,あなたが「myR1とmyR2が,単一のParmインスタンスを共有する」例えばR1でtimeを変更したらR2でも変更されていて欲しい,ことを目的とするなら,それはこっちです.上で定義した time は, myR1とmyR2で別の値を取ることが可能です. (もちろん, Paramクラスでtimeをdoubleではなくdouble* にしておき, 全てのtimeを一つの値に結合しておくとか, 値を同期する姑息な方法もありですが, まめな人じゃないと, 1週間後には結合し忘れてバグの素になるですよ.やはり結合し忘れるとビルドできない方が安全です )
継承できるのは, 変数だけではないです.
class Param
{
public:
double dt;
void Step()
{
//何か計算
}
};
ParamにはStep()メソッドが定義されています. すると, もちろんRegionクラスでも, myR1.Step() を実行することができます. ただし, 「何か計算」のところで利用できるデータは, 当然ながら, Paramクラスで利用できる変数だけです. ParamクラスはRegionがどうなっているのか分からないので,Regionクラス内部の変数を利用して計算することはできません.
- そのように考えると, Paramは「基底」なので保護者,Regionは「派生」なので子供たち,と考えるべきではないことになります.おうちの資産や経済状況などを全部知っているのは, 「派生」したRegionの方であり,こっちが保護者ですね. Paramは,小学校のことしかわからない子供たちの方です.
- 基底クラスは,限定された機能の一群を提供するべきです. 別の規定クラスは,別の機能の一群を提供する,というわけです.
継承したRegionが, Paramで定義されているStep()とは動作を変更したいとします. これをオーバーライドと言います:
class Region : public Param
{
public:
double dx;
double dt;
void Step()
{
//別の計算を記述できる.
Param::Step() ; // 基底クラスの関数を呼び出すことも可能
}
};
これを使うと, 「基本コース」を基底クラスに書いておき, 「特殊コース」を記述するのに派生クラスを作成し,必要な部分だけオーバーライドする, という芸ができるようになります.オーバーライドできるのは関数だけではなく, ここのdtのように変数もオーバーライドできます. こうすると, Parm::dt と書くと基底クラスのdtになり, dtと書けば現在の派生クラスのdtの値になります(変数が2つできている).
基底クラスのコンストラクタに引数が必須な場合, 何も書かないとエラーします. 対策例は次のとおり:
class Base
{
public:
Base(int ivalue){....}
};
class Plan1: public Base
{
public:
Plan1(int ivalue): Base(ivalue){....}
};
class Plan2: public Base
{
using Base::Base; // Baseと同じコンストラクタ作ってちょーだい
}
上の基底クラスBaseは, 引数のないコンストラクタがありません. そこで, 派生クラスPlan1では, 同一の引数のコンストラクタPlan1(int)を定義し, Baseのコンストラクタを呼び出してから自分のコンストラクタを実行しています. クソ面倒なので, 派生クラスPlan2では, using命令で「普通に定義してね」と頼んで済ませてしまってます. こっちの方法では, たとえBaseに17通りのコンストラクタがあっても, 全部自動で作ってくれますし, そのうち変更が必要なものがあれば, usingの下に追加して書けば上書きできます.
多重継承
基底クラスを継承して多数の派生クラスを作成し, それを継承してさらに別の派生クラスを作成できます. こうやって,ツリー状のクラスたちの構造ができます. ですが, 一つのクラスが多数の基底クラスを継承することも可能です.
class Region : public Param, public Velocity
{
public:
double dx;
};
このRegionはParamでもあり, Velocityでもあるので, 両方の機能が利用できます. これを多重継承と呼びます. ここで問題になるのは, VelocityもParamであった場合です. Region.dt とアクセスした時,
- Paramが保有するdt
- Velocity経由で保有するdt
の2通りがあり, どっちなのかが不明確になりますので, dtにアクセスすると, 「どっちやねん」となって,ビルドできません. 対策は, 重複する可能性があるクラスに virtualキーワードをつけることです:
cass Velocity : public virtual Param
{
...
};
class Region : public virtual Param, public Velocity
{
....
Region()
{
// dt と書いても, Param::dt と書いても, Velocity::dt と書いても, Velocity::Parm::dt と書いても, 同じものになる
}
};
これは, クラスが単純なツリー状の継承関係になっておらず, 先祖を辿ると別ルートで同一の基底クラスにたどり着くような,閉ループを構成していることを意味します.閉ループを含む継承を, 曖昧であると表現します. 閉ループが菱形の場合はダイヤモンド継承と呼んだりもします. ダイヤモンド継承が面倒なのは
- Param, Velocity, Geometryは他人が書いたもので変更できない. ソースコードもいちいち読んでなかった, とする.
- ここで, VelocityとGeometryをパーツとして継承したクラスRegionを作ろうとした
- ところが! なんかエラーするので, Param, Velocity, Geometryのソースコードを分析すると, VelocityはParamを, GeometryもParamを継承していた! virtual を入れたいのだが
- そもそも, あんたがパーツの詳細構造を読んで理解する必要があるのが, 不条理.
- 回路設計図を読まずにスイッチを入れると大爆発する炊飯器が売ってあったら,あんたもメーカーを訴えようと思うだろう,ふつうに.
- 「パーツの設計を変更して」と要求したら「たくさんの人が使っているからダメ」とか言われる
- あなたはParamが単一であって欲しいと思っているかもしれないが, Paramが複数であることを利用しているコードがすでに存在している可能性があるので, もう, 変更できないのだ
という事態が発生することです. これを指さしてオブジェクト指向はダメだ,と主張する人もいますが,まあ要するに継承を使いすぎなわけです.ダイヤモンド継承が出現したら,イエローカードが出たということで,使いすぎを反省することにすれば良いと思います.
- 実際には, VelocityとGeometryをパーツとして継承したクラスRegionにおいて, dt と書くと曖昧だからエラーするだけです.
- Velocity::dt, Geometry::dt と書くだけで, virtual なんて入れなくてもそれなりに使えます. ただし, Velocity::dt と Geometry::dt は,別の変数で共通の値にはならないのが気になるかもですね.
- virtural を入れると, Velocity::dt と Geometry::dt が同一のものになります.
- 上の例のように, ParamとVelocityをvirtual抜きで継承することだけはできません. この例ではParamの直接継承を削除するだけでうまく行きます. でも, virtual抜きではなぜダメなのか, 理由がわからんな?!? 文法上, 何がまずいんやろ?
アクセス範囲指定
ここまでの例では, 全て public の範囲で説明してきました. ですが, 基底クラスの作業が派生クラスから丸見えであると, 基底クラスをうっかり破壊してしまったりします. やはり, 不必要にオープンにしておくと危険です. 対策は
private変数・関数を利用する: 派生クラスからは, 外部からと同様に, 基底クラスでprivateなものにはアクセスできません.
protectedを利用する: 基底クラスで protectedなものは, 外部からはアクセスできませんが, 派生クラスからはpublic扱いになります.
多態(ポリモーフィズム)
継承を発展させると,
- 虫とは,羽ばたくものである. つまり Insect.Flap() が可能である.
- トンボは, 虫である. すなわち Flap()できる. 蝶も虫であり, Flap()できる.
- そこで, トンボを取り扱うプログラムと, 蝶を取り扱うプログラムを共通もののとし, いずれにせよ,Flap()せよ, と記述する
- これによって, 「そもそも虫とは⚪︎⚪︎⚪︎するものである」というふうに,抽象的で汎用性が高いプログラムが書ける
- トンボについて10万行書き, 蝶について10万行, ハエ,蛾, アブ・・・全昆虫合わせて100000万行のプログラムを作成していると, 何度も同じバグに遭遇して,前に進みません. そもそも虫について10万行書けば, 1度済んだ作業をもう1度行うことを防げるので, デバッグが高速になります.
これを実現するのが, ポインターの拡張です. ポインターは,もともとは「指し示す」変数でしたが,多態を実現する際に新しい機能が追加されました.これは
- 基底クラスのポインターは,派生クラスを指差しても良い
- 基底クラスのポインターを評価する場合,最初に,該当する派生クラスが何かを調べ,その派生クラス・基底クラスの値を評価する
としたものです. この該当する派生クラスが何かを調べるキーワードが virtual なのです. 基底クラスでは
class Insect
{
public:
virtual void Flap()
{
std::cout << "私は虫である" << std::endl;
}
};
派生クラスでは
class Dragonfly : public Insect
{
public:
void Flap()
{
std::cout << "あたいはドラゴンフライ" << std::endl;
}
};
さて, これを呼び出してみます.
Insect* P1=new Dragonfly();
P1->Flap(); // 私はドラゴンフライ
auto P2=*P1;
P2.Flap(); // 私は虫である
まずここでは, Dragonflyクラスのオブジェクトを作成していますが, それをInsectクラスのポインタ変数P1に代入しています. するとInsectクラスになりそうなものですが, ここでP1->Flap()を実行すると, 該当する派生クラスが何かを調べるが発動してしまい, 事実はDragonflyオブジェクトであったので,「私はドラゴンフライ」が実行されます.該当する派生クラスが何かを調べるのはポインタだけなので, ここで推定して実物にすると, P2はInsect扱いになってしまいます(だってP1はInsectへのポインターだから). というわけで,ここでは「私は虫である」が発動します.
例えば次のようなプログラムが可能になります
//初期設定部分
Insect* create_insect(std::string kind)
{
if (kind=="DF") return new Dragonfly();
if (kind=="F") return new Fly();
...
return nullptr;
}
//コア部分
...
auto INSECT=create_insect(user_input);
INSECT->Flap();
ユーザーが虫の種類kindを設定してプログラムを走らせると, 初期設定部分で虫を作成します. ここは虫の種類に依存しますが, だいたいここは短く書けます. で,本来プログラムが大変なコア部分では, 「虫とは⚪︎⚪︎⚪︎したり, ×××したり, △△△したりするものである」だけを使って記述するわけです.ここは,虫の種類に依存しないので,虫の種類が増えても変更する必要がなくなります.
- というわけで,多態プログラムを上手に書くには, まず,「そもそも,何と何を満たせば,虫であると断定できるのか」「何ができて何を所持するのが流体領域なのか」「何と何があれば気体分子であると言えるのか」「何と何を満たせばカレーと呼んで良いのか」「成分気体ふぜいが,そんな作業をする正当な権利を有しているのか」などを,よーく考える必要があります.
- 正当なオブジェクトが, 分相応なメソッドを行うことが決定した時, あなたのプログラムは,ほぼ自明なものになるでしょう.
該当する派生クラスが何かを調べる対象になっているFlap()のような関数を,仮想関数と呼びます.
そうすると, 元の基底クラスは,虫が満たすべき仮想関数を全て揃えている必要があります. が, 実際のFlap()の記述は派生クラスに行う.すると基底クラスで記述するものが無くなる場合があります.そういう場合には純粋仮想関数を書けば良いのです:
class Insect
{
public:
virtual void Flap()=0;
virtual void Proceed(){...もちろん定義できるなら普通に関数を定義しても良い...};
virtual void MoveUp()=0;
....
virtual ~Insect(){}
};
これら定義が=0の関数を含むクラスを抽象クラスとよびます. これは専業で基底クラスであり, オブジェクトを作ることができません(エラーしてビルドできない).必ず,派生クラスに継承し, そこで,=0になっている仮想関数を全て定義した時, 抽象が落ちて,ただの「クラス」になり,オブジェクトを作ることができるようになります.
さて, 上のコードで virtual ~Insect(){} と, virtual デストラクタを設定しているのには意味があります. 多態プログラムでは, もしかしてInsect* のオブジェクトを廃棄するかもしれないですよね. そうすると, Insectクラスのデストラクタが発動する・・・のは普通なのですが, もしかするとInsectの派生クラスInsectNextクラスでは, 特別なデストラクタ~InsectNext()を呼ばないとメモリーリークが発生する,なんてことも起こりえます. で, 基底クラスのデストラクタ~Insect()がvirtualではない場合・・・Insect*を破棄した時に, ~InsectNext()が発動しない! そこで, もう基底クラスであることがわかっているのなら, デストラクタをvirtual定義しておくのが安心安全なわけです. これは抽象であろうがなんだろうが, 基本的にそうしておいた方が未来が少し明るいです(こんなことは, 本来なら,プログラマーではなく言語仕様が配慮するべきことなのですがwww).
合成(カプセル)
プログラムを書いていると, この部分と, あの部分は, ある機能を共有しているということが頻繁に起こります.例えば
- 数値解析プログラムを書いていました. あるクラスAと, あるクラスBは, どちらも「時間 time」 という値を共有せねばならぬことに気づきました.
- 基本の計算式を矩形領域で実現するクラスを作成しました.ところが, 右側が無限遠方であるクラス, 上側が無限遠方であるクラスも必要になりました.ほとんど同じなんですが.
まあようするに, 微妙に機能が異なるプログラムを作りたい場合が, 多くあるということなのです.例えば,トンボというクラスと,チョウ,ハチ・・・いうクラス,という感じです.微妙に違いますが,全て飛行する昆虫であります.ですので,flap_down() flap_up() とか似たような関数で操作したいのですが,実際の内容が同じ場合,異なる場合がそれぞれある.こんなときには合成(encapsulate)機能を使います.これは,飛行虫クラスという共通機能のクラスを作り,それを拡張してトンボ,チョウ, etc. を作成する.
実は単純な話です. 次のプログラム
class Dragonfly
{
private:
std::vector<double> Position;
};
は, 配列機能std::vectorを合成してDragonflyクラスを使っている, と考えるわけです・・・使っているだけやんけ. でも, std::vectorの機能はちゃんと使えますよね. Positionのサイズを変更するのに, 普通は配列名.resize(...)を使っていたのを
class Dragonfly
{
public:
....
void resize(size_t N) { Position.resize(N);}
};
と書けば良いだけだからです. 使う機能の分だけ書かないといけないのが面倒です(定義方法も知っておかねばならんし)が, ダイヤモンド継承問題を突破するよりは楽です. PositionをDragonflyの外から勝手に操作できないように, private変数にしておくのも良いアイデアです(・・・もちろん面倒ですけど). すると
class Insect
{
public:
void Flap() { //Insectで基本的な羽ばたき方を記述 }
void MoveUp() { //Insectで基本的な上昇方法を記述 }
};
class Dragonfly
{
private:
Insect myInsect;
public:
void Flap() { myInsect.Flap();//Flapは基本通り }
void MoveUp() { //Dragonflyだけが利用する上昇方法 }
};
こんな感じになりますね. もちろん, Insectのメンバー変数はDragonflyでは利用できないので・・・利用できるように工夫する必要があります.
- Flap()で必要なデータはInsectに存在しないとまずい. Dragonflyでそれを操作するには, public変数にするか(←悪手です), 操作する関数(C#でいうプロパティ操作)をInsectに定義し, myInsect.関数() で呼び出す.
- 後者はInsectのメンバーの操作はInsectが行うことになるので, 他人を含め数ヶ月以上の長期間利用するプログラムでは可読性が向上します.
- Dragonflyに特徴的なデータはもちろんDragonflyに定義するので, それを操作するプログラムはInsectには書けない
- Dragonflyのメンバーの操作はDragonflyに記述してあるのが筋ですよね. それに逆らうと, 読めないプログラムが出来上がります.
データ共有
合成では, クラスの複数のインスタンスが同一のデータを共有する(FortranでいうCommon Block, Cでいうglobal変数チックなもの)を実現できます. 例えば
class Param
{
public:
double time;
};
class Region
{
Param myParameter;
public:
double& time() { return myParameter.time; }
};
こうやって定義してしまうと, Regionインスタンスが2つあると, time も2つあることになってしまいます:
...
Region R1, R2;
R1.time()=1.2; R2.time()=2.3; // 二つのRegionは, 自分が保有するmyParameterに, 別々の時刻 time を保有してしまう
そうではなくて, システムに共通の「時刻」を使いたい場合は, 共有するParamをRegionに与えれば良いのです:
class Region
{
Param& myParameter;
public:
Region(Param& param):myParameter(param){}
...
};
...
Param YHWH; // この世界に存在する唯一のパラメータ YHWH を定義
Region R1(YHWH), R2=R1; // 神の名YHWHを与えて領域を初期化
std:vector<Region> Regions(20,YHWH);
Regions.resize(3,YHWH);
注意しなければならないのは, 共有するために
- ここが参照になる
- もちろんポインターでも良いが, そうすると神が居ない間に誕生可能になってしまい,あなたが誕生後に神を与えるコードを書き忘れるとバグになる
- 実行後にバグが判明するより, ビルドする時にエラーしてくれた方が有難い場合は, 参照にしましょう.
- ビルドでエラーせず,実行してもバグが露見するとは限らないというのが好みなら,ポインターでどうぞ
- 参照になると,デフォルトコンストラクタが使えないので定義する必要がある. コンストラクタでは, 参照を初期化しないとダメ
- Regionを初期化するのに, 神の名YHWHを与える必要が出てくる. あるいは信者のコピーを作るか. だな.
- Regionは信仰心を持っているので, なにをするにも, いちいち神の名を唱える必要が出てくる
こうしておけば, この世界に存在するParamインスタンスはYHWHだけであるので,
R1.time()=1.3; // この世界の時間を1.3に設定 (神の時間をR1が定めるのは,畏れ多い気もするが)
std::cout << R2.time() << std::endl; // 世界において時間は唯一であるので, R2 でもちゃんと1.3になっている!
継承(インヘリット), 合成(エンカプシュレーション), 多態(ポリモーフィズム)の3つを合わせて, 「オブジェクト指向 三つの奥義」と呼ぶそうです.
はじめての並列処理
最近の計算機は, 1粒のCPUで,同時に多数の作業ができるように工夫されています.これには,幾つかの種類があります.これを理解するために,数値計算とは何をしているのか,説明しましょう.
- 計算機では,多数の「変数」の数値を記録しておくことができる.
- プログラムというものは,その変数の値を,与えられた手順で変更することができるものである.
上の「記録」が,コンピューターで,「メモリー」と呼ばれるものです.なお,メモリーには二種類あって
- 大容量だが,アクセス速度が遅い.例えば, USBメモリー,SSDディスク, ハードディスク,フロッピーディスク,磁気テープ,穴あき紙テープなど.円盤形が多いので,「ディスク」と呼ばれることもありますが,USBメモリーは円盤形ではありませんね・・・
- 小容量だが,アクセス速度が速い.メインメモリーと呼ばれるICチップです.これの極端なものとして,CPU内部には,ごく微量だが,超高速のメモリーもあります.それは特に「キャッシュ」と呼ばれます.
- メインメモリーの一部を,あなたのプログラムに割り当てます.
- プログラムは,割り当てられたメインメモリー(メモリー空間)の数値を,定められた手順で変更します.
プログラムが走っている状態を,「プロセス」と呼びます.1980年という古代には,プロセスが数値を変更するとき,同時に1つの数値しか変更することができませんでした.ですが,現代のコンピュータは,1つのプロセスが同時に多数の数値を変えることができます.これを「マルチスレッド」と呼びます.同時に数値を変更する,といっても無秩序に変更するのではなく,「スレッド」1つは,順序良く数値を変更します.多数の「スレッド」が,同一のメモリー空間内部の,異なる変数の値を変更していくわけです.このやり方は,簡単で無駄(オーバーヘッド)がありませんが,多数のスレッドが,同一時刻に同一変数にアクセスすると,異常な結果が出てきます.そこで,同一アクセスを避ける仕組み:排他制御が必須になり,プログラムそのものに,多少の工夫が必要です.
いちいち排他制御をプログラムに書くのは面倒であるので,特定のfor(...)ループでのみ,排他制御を自動的に行い,マルチスレッドの恩恵を受けようとする考えがあります.その代表例がOpenMPと呼ばれるものです.
生のマルチスレッドでは,いちいちプログラムで排他制御するのは面倒だし,OpenMPでは並列処理はfor(..)ループだけです.そこで,別の考え方の並列処理があります.「マルチプロセス」です.これは,つぎのような考え方です.
- 一つあるいは遠隔地にある複数のコンピューターで,プロセスを多数起動する.
- それぞれのプロセスは,固有のメモリー空間を持つ.
- プロセス同士は,送信(send), 受信(receive)機能を持っており,相互にデータをやり取りしして協調動作する
send, receive ができさえすれば,どのように書いても良いです.ですが,面倒を避けるために,専用のライブラリーソフトが各種出ています.最も代表的なものが,MPI (メッセージ渡しライブラリー)です.MPIライブラリーを利用する場合,「プロセスを多数起動する」ことが必要になるので,プログラムの起動の方法が異なります.つまり
- 通常のプログラムや,マルチスレッドプログラムは, 「プログラム名を入力してエンターキーを押す」とか「プログラム名をマウスでクリックする」で起動
- MPIプログラムは,MPI起動プログラム(mpirun とか)にプログラム名を与えて,起動してもらう.
MPI並列計算は,自分のメモリーの数値を計算している限りは,排他制御を考える必要がありません.そこは楽ですが,並列していない部分もプログラムには含まれますし,また,通信でデータを渡す以上は,むだ時間「オーバーヘッド」があります.通信量がちょびっと上がるだけで,激烈に計算速度が低下しますので,通信サイズが少なくなるように切磋琢磨する必要が出てきます.
文字列→変数
文字列を変数に直すのは,やり方が色々あります.非常に極端な文字表現もあり得ますので・・・
ですが,普通は次のでOK
- std::stod(倍精度みたいな文字列)
- std::stoll(long longに変換できそうな文字列)
- std::stoull(unsigned long longに変換できそうな文字列)
などなど.
マルチスレッド
本文
CやFortranの関数の呼び出し
本文
マルチプロセス
本文
プロセス間通信
本文
名前空間
本文