1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

今までの流れ

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

今回やったこと

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

  • まず,c++の基本
  • クラスの設計・実装
    • 名前空間
    • 相互参照
    • メンバ変数,メンバ関数
    • コンストラクタ,デストラクタ
      • コピーコンストラクタは扱わない
    • protectedpublicfriend
    • 自己参照
      • 実質的には再帰処理
  • 演算子のオーバーロード
    • シフト演算子
      • ある意味でストリーム的に活用
    • コンマ演算子
      • 区切りとしての意味合いを活用
  • Eigenもどきはまだ未完成なのでまた今度
    • こっちの演算子のオーバーロードは特殊
    • コピーコストと実用性の狭間で揺れ動く
    • もしかしたら公開できないかもしれない

お題

  • Eigenmatrixを相手に<<を挟んで数字+カンマを含めた入力を扱うことができる
    • M << 1,2,3;
    • ストリームみたいだがストリームではない,たまたまiosクラスが<<>>を使ってるだけ,これはEigenに固有の実装らしい
  • これはとても便利なので,まずはその雛形を作る
    • 入力して出力できればOKです

以下,できたもの

../MyIO/Catcher.hpp
#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との連携を許可する
    };

};
../MyIO/Grabber.hpp
#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との連携を許可する
    };

};
../MyIO/Catcher.cpp
#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();
};
../MyIO/Grabber.cpp
#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が勝手に再起処理を行う
};
makefile
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)
main.c
#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

つまり

  • 2種類の演算子にオーバーロード
  • クラスに自己参照・相互参照のシステム

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

感想

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

参考

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

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

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

明示的初期化について

thisポインタについて

<vector>について

friendについて

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

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

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

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

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?