注意事項
- 2週間くらい1人で適当に弄った結果,アテにならん
- 静的リンクだけやってる
- 動的リンクはやらない
- 動的ロードもやらない
- あくまで過酷環境の組込実装のためにやってる
- 動的リンクも動的ロードも無理らしい…
- 暇があれば後でやってみようと思ってる
- スマートポインタは使わない
c++98
を想定してる- スマートポインタは
c++11
以降の機能なので
概要
- プロジェクトの構成
- 認識のしやすさ
- 管理のしやすさ
- モジュールとして分割して開発
makefile
の改造- シェルの文法なのか
make
の文法なのかこれもうわかんねぇな make
の機能だけで色々やろうとしたが個人的には無理だった- 文法の縛りがキツくて関数の実用性をあまり感じなかった
- もうシェルスクリプトを叩く方が早い?初期化もシェルで叩く?
- モジュール追加時の自動構成もできると気持ちが良さそう
- シェルの文法なのか
- クラス
x
をメンバ変数に持つクラスy
を定義するヘッダファイルでは,前方宣言でx
を定義してinclude
の多重ロードを防止する- つまりヘッダファイルでは
x
を定義してinclude
せずに,ソースファイルではx
を定義するヘッダファイルをinclude
する - たとえば「「「クラス
x
」を抱えるクラスy
」を抱えるクラスz
」を定義する時にヘッダファイルに依存関係を全て表示すると,y
を定義するヘッダファイルに加えてy
を経由してx
を定義するヘッダファイルまでロードする,といった多重インクルードが発生する- 何レイヤも下にあるライブラリのヘッダファイルを遡上するのは,まともにやろうとしても
include path
の管理が壊れる - 名前空間の管理も高レベルになる,ヘッダファイルの挙動によっては思わぬ動作を生むかもしれない,俺は辛い耐えられない
- 1つのライブラリを複数のソースで構成して
include
するならOK,ただしそれでもinclude path
には十分に注意する
- 何レイヤも下にあるライブラリのヘッダファイルを遡上するのは,まともにやろうとしても
- つまりヘッダファイルでは
- そのような実装をするクラス
y
はクラスx
をメンバ変数に持つが,コンパイラの都合によって実体ではなくポインタで持つ必要があり- たとえば「「「クラス
x
」を抱えるクラスy
」を抱えるクラスz
」の実装をコンパイルする際は,多重インクルードが阻止されているためにx
のサイズが不定となり,つまりy
のサイズも不定となり,そしてx
のサイズも不定となるため,普通にエラーを吐かれる- もっと簡単に表現すると2つ下のレイヤで定義されたクラスのサイズがわからないから実体を持つとコンパイルできない
- 実体を持たずにポインタを持つならば,ポインタのサイズは処理系によれどいずれも既知となるはずなので,この問題は起こらない
- ある意味でシンボル指向に基づくオブジェクト指向みたいな感覚を覚えた,普段からリンクする時のことを考えた方が良い
- ユニークポインタが未実装の
c++98
ではnew
とdelete
を自力で管理しないとメモリリークするので,気をつけるべし - さらに言えばヘッダファイルが隔離されているせいで2つ下レイヤのクラスにあるメソッドや変数は叩けない,気をつけるべし
- たとえば「「「クラス
- 静的ライブラリの取り扱いについて
- 実行ファイルの生成時にシンボルの多重定義を防止するには,ライブラリ毎に生成した静的ライブラリを最後にまとめて呼び出す(普通はこうするっぽい)か,依存する静的ライブラルをバラして得たオブジェクトとコンパイルで得たオブジェクトをまとめてマージして重複を消し飛ばす作業を,ライブラリ毎の静的ライブラリの生成からメインのオブジェクトが呼ぶ静的ライブラリの生成まで一貫してやり続けるか…
- 前者はライブラリの依存関係がソースファイルの
include
の依存関係に一致しないので.リンカに渡すパスの管理が面倒 - 後者はディスクを無駄に消費する,ライブラリ毎に設定されるべき責任境界が壊れる,実装の変更の反映も簡単にできない
- 前者はライブラリの依存関係がソースファイルの
- ちなみに過酷環境の組込実装では浮動小数点演算が
run-time-support
のライブラリとして提供されている場合があるらしい
- 実行ファイルの生成時にシンボルの多重定義を防止するには,ライブラリ毎に生成した静的ライブラリを最後にまとめて呼び出す(普通はこうするっぽい)か,依存する静的ライブラルをバラして得たオブジェクトとコンパイルで得たオブジェクトをまとめてマージして重複を消し飛ばす作業を,ライブラリ毎の静的ライブラリの生成からメインのオブジェクトが呼ぶ静的ライブラリの生成まで一貫してやり続けるか…
プロジェクトの構成
以下は一例
. ├── bin │ └── main ├── libA │ ├── inc │ │ └── libA.hpp │ ├── lib │ │ └── liblibA.a │ └── src │ ├── clsA.cpp │ └── clsA.hpp ├── libB │ ├── inc │ │ └── libB.hpp │ ├── lib │ │ └── liblibB.a │ └── src │ ├── clsB.cpp │ └── clsB.hpp ├── makefile └── src └── main.cpp
規則
- モジュール毎にディレクトリを設置
- その下は
inc
とlib
とsrc
に分割 src
にはソースファイルとヘッダファイルを設置lib
には静的ライブラリを設置inc
のはsrc
にあるヘッダファイルを全てinclude
した代表ヘッダファイルを設置- 他モジュールやメインモジュールから参照されるヘッダファイルはこの1つに統一する
- モジュールのコンパイル時にソースファイルに応じて
makefile
で自動的に生成する - ただし
include path
はsrc
の方にも通す,代表ヘッダファイルが呼び出すので
makefile
の改造
以下は一例
SHELL = /bin/zsh MAIN_SRC = src/main.cpp MAIN_OBJ = main.o MAIN_BIN = bin/main GCC_BIN = clang++ GCC_FLG = -std=c++98 -O2 make : make libA make libB make main libA : FORCE $(call setAllVar,libA) $(GCC_BIN) $(GCC_FLG) -c $(libA_SRC) @ar cr $(libA_OBJ) *.o @rm -f *.o $(shell echo '#pragma once' > $(libA_REP);) $(foreach hed,$(libA_HED),$(shell echo '#include "$(hed)"' >> $(libA_REP);)) libB : FORCE $(call setAllVar,libB) $(call setRefVar,libA) $(GCC_BIN) $(GCC_FLG) -c $(libA_INC) $(libB_SRC) @ar x $(libA_OBJ) @ar cr $(libB_OBJ) *.o @rm -f *.o __.* $(shell echo '#pragma once' > $(libB_REP);) $(foreach hed,$(libB_HED),$(shell echo '#include "$(hed)"' >> $(libB_REP);)) main : FORCE $(call setRefVar,libB) $(GCC_BIN) $(GCC_FLG) -c $(libB_INC) $(MAIN_SRC) @ar x $(libB_OBJ) $(GCC_BIN) $(GCC_FLG) -o $(MAIN_BIN) $(MAIN_OBJ) $(libB_LIB) @rm -f *.o __.* FORCE : define setAllVar $(eval $1_HED = $(notdir $(wildcard $1/src/*.hpp))) $(eval $1_SRC = $(wildcard $1/src/*.cpp)) $(eval $1_OBJ = $1/lib/lib$1.a) $(eval $1_REP = $1/inc/$1.hpp) $(eval $1_INC = -I$1/inc -I$1/src) $(eval $1_LIB = -L$1/lib -l$1) endef define setRefVar $(eval $1_OBJ = $1/lib/lib$1.a) $(eval $1_REF = $1/inc/$1.hpp) $(eval $1_INC = -I$1/inc -I$1/src) $(eval $1_LIB = -L$1/lib -l$1) endef
説明
shell
はzsh
を指定する- ただしシェルスクリプトの中で
make
の機能を叩くのは無理- 関数を書いても実用性を感じられなかったのはこのため
- せいぜい変数を自動設定するくらいしかできなさそう…
- 存在するソースに応じ代表ヘッダファイルを自動的に生成する
$(foreach)
して$(shell echo)
したらできた…
- 空変数
FORCE
を使って常に強制的にmake
を実行させる- モジュール毎にコンパイルするだけなら十分だと思った
-L
は静的ライブラリの探索パスでリンク時に使う,-I
はヘッダファイルの探索パスでコンパイル時に使う- リンク時に
-L
と一緒に使う-l
はライブラリを指定するが(lib)libA.a
みたいに先頭のlib
が削れるらしい
静的ライブラリの取り扱い
バラしてマージするパターンを実装した
ar x libName
でバラしてar cr libName *.o
でマージしてrm *.o __.*
で一時ファイルを消す
macOSに入るcommand line tool
のclang++
は__.SYMDEF
なるものを出すが,たぶんgnu
のclang++
でも同じことが起きる
見た感じシンボルのリスト(が短縮されたもの)が入ってるテキストファイルっぽかったが,用途がよくわからない,調べる必要があるかも
多重ロード防止実装
以下は一例
libA
#pragma once namespace libA{ class clsA{ private: int __Num; protected: int _Num; public: clsA(); ~clsA(); int Num; void aLog(); }; }
- 3レベルそれぞれのアクセス権限に1つずつ変数を置く
#include <iostream> #include "clsA.hpp" using namespace std; using namespace libA; clsA::clsA(){ cout << "construct A" << endl; __Num = 0; _Num = 1; Num = 2; } clsA::~clsA(){ cout << "destruct A" << endl; } void clsA::aLog(){ cout << " log from A" << endl; cout << " private num: " << __Num << endl; cout << " protected num: " << _Num << endl; cout << " public num: " << Num << endl; }
libB
#pragma once namespace libA{ class clsA; } namespace libB{ class clsB{ private: libA::clsA* __aObj; protected: libA::clsA* _aObj; public: libA::clsA* aObj; clsB(); ~clsB(); void bLog(); }; }
- 1つ下のレイヤのクラスをポインタで持つ
- 3レベルそれぞれのアクセス権限に1つずつ置く
#include <iostream> #include "libA.hpp" #include "clsB.hpp" using namespace std; using namespace libA; using namespace libB; clsB::clsB(){ cout << "construct B" << endl; __aObj = new clsA(); _aObj = new clsA(); aObj = new clsA(); } clsB::~clsB(){ cout << "destruct B" << endl; delete __aObj; delete _aObj; delete aObj; } void clsB::bLog(){ cout << "log from B" << endl; cout << " private A:" << endl; __aObj->aLog(); cout << " protected A:" << endl; _aObj->aLog(); cout << " public A:" << endl; aObj->aLog(); }
new
とdelete
でメモリを管理するc++11
以降はユニークポインタを使うべき?
- ポインタで持つので
.
ではなく->
で変数やメソッドを呼び出す
main
#include <iostream> #include "libB.hpp" using namespace std; using namespace libB; int main(){ clsB* bObj = new clsB();; bObj->bLog(); delete bObj; };
- クラス設計するわけでもないので,オブジェクトは直置きできるはず
- が,
new
とdelete
を使った方がコーディング規約的に楽かも
- が,
main
からは2つ下のレイヤのclsA
の変数とメソッドを叩けない- ある意味では意図せずにセキュアな実装になっててビックリした
実行
% make make libA clang++ -std=c++98 -O2 -c libA/src/clsA.cpp make libB clang++ -std=c++98 -O2 -c -IlibA/inc -IlibA/src libB/src/clsB.cpp make main clang++ -std=c++98 -O2 -c -IlibB/inc -IlibB/src src/main.cpp clang++ -std=c++98 -O2 -o bin/main main.o -LlibB/lib -llibB % bin/main construct B construct A construct A construct A log from B private A: log from A private num: 0 protected num: 1 public num: 2 protected A: log from A private num: 0 protected num: 1 public num: 2 public A: log from A private num: 0 protected num: 1 public num: 2 destruct B destruct A destruct A destruct A
大域変数に自作クラスを使うと…
過酷環境の組込実装では初期化と割込制御をint main(){}
とinterrupt void xxx(){}
のように実装するので,例えば制御器は大域変数を介して動作する必要がある
それぞれの関数や変数は別のソースに書いてextern
してまとめてコンパイルすれば使えるが,そういうやり方ではいつか地獄を見るのでよろしくないとされる
では大域変数として制御器の機能を持つ自作クラスをオブジェクト化するとどうなるのかと言えば,(処理系によって違うが)main
の実行前の処理にオブジェクトのコンストラクタが走るように登録されたり,実行後の処理にデストラクタが走るように登録されたりする,これもrun time support
のライブラリで提供される機能となる
しかし実質的にmain
が初期化ルーチンとなっているなら,クラスのポインタを大域変数に置きつつ後でnew
で実体化しても良さそうなのだが,するとdelete
ってどうやって登録すれば良いのだろうか…?