オレオレクラスによるシフト(ストリーム)演算子とコンマ(カンマ)演算子オーバーロードを利用したオレオレライブラリ

今までの流れ

  • ここ最近の最終目標は行列計算を使った組込制御
  • ところで「そのコンパイラならc++が使えるかも」と通達された
    • えっclassと標準ライブラリ使っていいのか!?やったー
    • 本当にc++が通った,今まで誰もやってないだけだった…
  • ただしc++98標準にのみ対応,もしくはc99標準にのみ対応
    • かつCPUが特殊で一般的なコンパイラLLVM等)が使えない
    • アライメントの扱いが特殊なせいかEigenが使えなかった
      • オブジェクト生成+リンクはおろかヘッダ導入すら無理
    • vectorはいける感があるらしい,それでなんとかしたい
  • Eigenなし-std=c++98縛りで,似たようなものを実装する

今回やったこと

c++入門ついでにオブジェクト指向コーディング

お題

  • Eigenmatrixを相手に<<を挟んで数字+カンマを含めた入力を扱うことができる
    • 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;

に対して

  1. 親クラス生成
  2. 算術演算(+-*/)の発生
    • いずれも<<,より優先度が高い
  3. <<演算,親クラス起爆+データ取る+親クラスの参照を持つ子クラス生成
  4. ,演算,子クラス起爆+データ取る(親クラス参照)+子クラス自己参照
  5. ,演算,子クラス起爆+データ取る(親クラス参照)+子クラス自己参照
  6. (以下;が来るまで演算の再帰処理が発生,;が来れば勝手に終了)

を実装すればよい

説明.png

つまり

があればOKと言える

実行結果について

CreateDeleteCatcherGrabberの生成と削除,AlternGrabberの再起 Catcerは1回の生成と削除 Grabberは入力の度に生成と削除,および引数の分だけ再起

名前空間

ヘッダでもソースでも,ファイルが異なっても名前空間は共用できた,便利

相互参照とprotectfriendの併用

クラス定義する前の相互参照の宣言と,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でまとめて定義した方が管理が楽かも しかし()だけはメンバ関数でやらないといけない あと参照受けするとコピーコスト少ないんじゃないか ちなみに三項演算子オーバーロードできないらしい

感想

マジでなんもわからん状態からここまで持ってこれたのでインターネットの集合知に感謝してる 必要に迫られ次第で年末年始関係なくコーディングが楽しくなってくるってはっきりわかんだね

参考

https://qiita.com/h_hiro_/items/a6484101d87847299885

実際はこちらを再構成したものになりますが,解読と解釈を入れて個人的に読みやすくしました @h_hiro_ さん,大変ありがとうございます,-std==c++98設定でもちゃんと動きました

https://qiita.com/tediousWerk/items/04e61864a20afb4ecee7

右辺にthisポインタがない事情について

https://brain.cc.kogakuin.ac.jp/~kanamaru/lecture/C++2/09/09-03.html

コピーコンストラクタについて

https://hakase0274.hatenablog.com/entry/2019/09/28/200000

明示的初期化について

http://wisdom.sakura.ne.jp/programming/cpp/cpp15.html

thisポインタについて

https://cpprefjp.github.io/reference/vector/vector.html

<vector>について

https://skpme.com/662/

friendについて

https://pknight.hatenablog.com/entry/20090830/1251603528

friend演算子オーバーロードのコンボについて

https://www.ibm.com/docs/ja/xl-c-and-cpp-aix/13.1.0?topic=only-variadic-templates-c11

可変数引数テンプレートというのもあったがそもそもEigenのやり方が便利なので採用しなかった

https://kaworu.jpn.org/cpp/%E3%82%AA%E3%83%BC%E3%83%90%E3%83%BC%E3%83%AD%E3%83%BC%E3%83%89

オーバーロードの概要と事例

https://marycore.jp/prog/cpp/function-object/

()オーバーロードは関数オブジェクトと呼称されるらしい

https://programming-place.net/ppp/contents/cpp/language/019.html

https://programming-place.net/ppp/contents/cpp/language/034.html

あとはここがとても詳しかったと思う