今までの流れ
- ここ最近の最終目標は行列計算を使った組込制御
- ところで「そのコンパイラなら
c++
が使えるかも」と通達された- えっ
class
と標準ライブラリ使っていいのか!?やったー - 本当に
c++
が通った,今まで誰もやってないだけだった…
- えっ
- ただし
c++98
標準にのみ対応,もしくはc99
標準にのみ対応- かつCPUが特殊で一般的なコンパイラ(LLVM等)が使えない
- アライメントの扱いが特殊なせいか
Eigen
が使えなかった- オブジェクト生成+リンクはおろかヘッダ導入すら無理
-
vector
はいける感があるらしい,それでなんとかしたい
-
Eigen
なし-std=c++98
縛りで,似たようなものを実装する
今回やったこと
c++
入門ついでにオブジェクト指向コーディング
- まず,
c++
の基本 - クラスの設計・実装
- 名前空間
- 相互参照
- メンバ変数,メンバ関数
- コンストラクタ,デストラクタ
- コピーコンストラクタは扱わない
-
protected
・public
・friend
- 自己参照
- 実質的には再帰処理
- 演算子のオーバーロード
- シフト演算子
- ある意味でストリーム的に活用
- コンマ演算子
- 区切りとしての意味合いを活用
- シフト演算子
-
Eigen
もどきはまだ未完成なのでまた今度- こっちの演算子のオーバーロードは特殊
- コピーコストと実用性の狭間で揺れ動く
- もしかしたら公開できないかもしれない
お題
-
Eigen
はmatrix
を相手に<<
を挟んで数字+カンマを含めた入力を扱うことができるM << 1,2,3;
- ストリームみたいだがストリームではない,たまたま
ios
クラスが<<
と>>
を使ってるだけ,これはEigen
に固有の実装らしい
- これはとても便利なので,まずはその雛形を作る
- 入力して出力できればOKです
以下,できたもの
#pragma once
#include <vector>
#include <Grabber.hpp>
namespace MyIO{
class Grabber;
//相互参照を設定する
class Catcher{
protected:
std::vector<long double> arr;
//データを保存する
public:
Catcher();
//コンストラクタ
~Catcher();
//デストラクタ
Grabber operator<<(long double num);
// データを"<<"で取る
// Grabberを呼び出す
void drainup();
//溜めたデータを出す
friend class Grabber;
//Grabberとの連携を許可する
};
};
#pragma once
#include <Catcher.hpp>
namespace MyIO{
class Catcher;
//相互参照を設定する
class Grabber{
protected:
Catcher &catcherMem;
//Catcherの参照を保存する
//(データを返すのに使う)
public:
Grabber(Catcher &catcherRef):catcherMem(catcherRef){};
//Catcherが呼び出せる初期化を定義する
//Catcherの参照をメンバ変数に保存する
//今回は明示的な初期化を行わないとダメ
~Grabber();
Grabber& operator,(long double num);
//データを","で取る
//自身のポインタを返して再帰処理を行う
friend class Catcher;
//Catcherとの連携を許可する
};
};
#include <iostream>
#include <Catcher.hpp>
#include <Grabber.hpp>
using namespace MyIO;
Catcher::Catcher(){
std::cout << "Create Catcher" <<std::endl;
};
Catcher::~Catcher(){
std::cout << "Delete Catcher" <<std::endl;
};
Grabber Catcher::operator<<(long double num){
arr.assign(1,num);
//長さ1で初期化して,その先頭にnumを代入する
std::cout << "Create Grabber" <<std::endl;
return (Grabber(*this));
//自身の参照を渡してGrabberを起爆する
//以降はGrabberが勝手に再起処理を行う
};
void Catcher::drainup(){
for (size_t i = 0; i < arr.size(); i++){
std::cout << arr[i] << " ";
}
std::cout << std::endl;
arr.clear();
};
#include <iostream>
#include <Grabber.hpp>
#include <Catcher.hpp>
using namespace MyIO;
Grabber::~Grabber(){
std::cout << "Delete Grabber" <<std::endl;
};
Grabber& Grabber::operator,(long double num){
std::cout << "Altern Grabber" <<std::endl;
catcherMem.arr.push_back(num);
// 最後にnumを代入する
return (*this);
//自身の参照を渡してGrabberを起爆する
//以降はGrabberが勝手に再起処理を行う
};
NAME = test.exe
SRMN = main.cpp
SRSB = myIO/Catcher.cpp myIO/Grabber.cpp
INCL = myIO
OBMN = test.exe
OBSB = $(SRSB:%.cpp=%.out)
GCCC = clang++
GCOP = -std=c++98
make : $(SRMN) $(SRSB)
$(GCCC) $(GCOP) $(SRMN) $(SRSB) -I $(INCL) -o $(OBMN)
#include <Catcher.hpp>
#include <Grabber.hpp>
using namespace MyIO;
int main(){
Catcher foo;
foo << 1, 2, 3, 4, 5;
foo.drainup();
foo << 6, 7, 8, 9, 10;
foo.drainup();
long double bar = 1;
foo << bar/1, bar/2, bar/3, bar/4, bar/5;
foo.drainup();
};
実行結果
% make
clang++ -std=c++98 main.cpp myIO/Catcher.cpp myIO/Grabber.cpp -I myIO -o test.exe
% ./test.exe
Create Catcher
Create Grabber
Altern Grabber
Altern Grabber
Altern Grabber
Altern Grabber
Delete Grabber
1 2 3 4 5
Create Grabber
Altern Grabber
Altern Grabber
Altern Grabber
Altern Grabber
Delete Grabber
6 7 8 9 10
Create Grabber
Altern Grabber
Altern Grabber
Altern Grabber
Altern Grabber
Delete Grabber
1 0.5 0.333333 0.25 0.2
Delete Catcher
解説
アルゴリズム
要するに
M;
M << fa,fb,fc;
に対して
- 親クラス生成
- 算術演算(
+
,-
,*
,/
)の発生- いずれも
<<
と,
より優先度が高い
- いずれも
-
<<
演算,親クラス起爆+データ取る+親クラスの参照を持つ子クラス生成 -
,
演算,子クラス起爆+データ取る(親クラス参照)+子クラス自己参照 -
,
演算,子クラス起爆+データ取る(親クラス参照)+子クラス自己参照 - (以下
;
が来るまで演算の再帰処理が発生,;
が来れば勝手に終了)
を実装すればよい
つまり
- 2種類の演算子にオーバーロード
- クラスに自己参照・相互参照のシステム
があればOKと言える
実行結果について
Create
とDelete
はCatcher
とGrabber
の生成と削除,Altern
はGrabber
の再起
Catcer
は1回の生成と削除
Grabber
は入力の度に生成と削除,および引数の分だけ再起
名前空間
ヘッダでもソースでも,ファイルが異なっても名前空間は共用できた,便利
相互参照とprotect
とfriend
の併用
クラス定義する前の相互参照の宣言と,protect
に接続させるfriend
の宣言は,両方とも必要だった
private
を選ばなかったのはクラス継承で弄りたくなった時に痛い目をみそうな予感がしてしまったから
コンストラクタ・デストラクタ
ロギングによって追跡できるようにした,これに加えてコピーコンストラクタもあればコストが把握できる
myClass::myClass(){}; //デフォルトコンストラクタ
myClass::myClass(class arg){}; //引数ありコンストラクタ
myClass::~myClass(){}; //デフォルトデストラクタ,引数はない
myClass::myClass(const myClass &myObject){}; //コピーコンストラクタ,引数は自クラスのオブジェクトの参照
明示的初期化
いまいちよくわからないが(日本語で検索しても引っ掛からなかった)多分
- デフォルトコンストラクタが定義されていない
- 自明なデフォルト値がないメンバ変数を抱える
クラスでは一発目の定義の時点でメンバ変数を初期化する必要があるらしい
myClass::myClass(class arg):var(arg){}; //明示的初期化
今回はGrabber
で引っかかりコンパイルエラーを吐かれたので修正した
なお,引数を加工するメンバ関数をコンストラクタに渡すのはアリらしい
int myClass::fcn(class arg){return arg;}; //引数を加工できる関数を用意する
myClass::myClass(class arg):var(fcn(arg)){}; //それを使って初期化を工夫する
ライブラリ設定
コンパイル時の命令で-I $(PATH)
すれば良い,そうすると<myHeader>
記法が使えて便利
演算子オーバーロード
以下は一例
classReturn operator _operator_ (class1 arg1[,class2 arg2]){
classReturn a = fcn(arg1[,arg2]);
return a;
};
_operator_
には使いたい演算子を設定する(ただし設定できないものもある)
演算子オーバーロードをクラス内のpublic
なメンバ関数で定義しており,かつ左辺の変数のクラスが定義しているクラスと一致する場合は,引数に設定するのは右辺の変数のみで済む(その際this
ポインタが左辺の変数を指すため)(下記のような書き方もできなくはないらしい)
右辺の変数に定義しているクラスの変数を使いたいような場合は,(右辺の変数,左辺の変数)の順で引数を2つ定義した上で,アクセスしたいメンバ変数やメンバ関数をpublic
にするか,protected
にした上で演算子オーバーロードにfriend
に設定する必要があるっぽいな?
要はfriend
でまとめて定義した方が管理が楽かも
しかし()
だけはメンバ関数でやらないといけない
あと参照受けするとコピーコスト少ないんじゃないか
ちなみに三項演算子はオーバーロードできないらしい
感想
マジでなんもわからん状態からここまで持ってこれたのでインターネットの集合知に感謝してる
必要に迫られ次第で年末年始関係なくコーディングが楽しくなってくるってはっきりわかんだね
参考
実際はこちらを再構成したものになりますが,解読と解釈を入れて個人的に読みやすくしました
@h_hiro_ さん,大変ありがとうございます,-std==c++98
設定でもちゃんと動きました
右辺にthis
ポインタがない事情について
コピーコンストラクタについて
明示的初期化について
this
ポインタについて
<vector>
について
friend
について
friend
と演算子オーバーロードのコンボについて
可変数引数テンプレートというのもあったがそもそもEigen
のやり方が便利なので採用しなかった
オーバーロードの概要と事例
()
のオーバーロードは関数オブジェクトと呼称されるらしい
あとはここがとても詳しかったと思う