概要
前回のあらすじ
分割コンパイルでモジュール開発をする場合は,ライブラリのシンボル指向実装とinclude
地獄の回避可能なヘッダ記述手法の都合から,モジュールが異なる(レイヤが1つ下の)自作クラスをメンバ変数に持つなら,実体ではなくポインタで持った方が良く,かつそれらはnew
・delete
を使用してメモリ管理をちゃんと実装し,コンストラクタもデストラクタも確認するべし,的な結論に至った(c++98
縛りゆえスマートポインタ不使用)
しかしモジュールの開発においては,同一レイヤのレベルで自作クラスを実装して,そのまま自作クラスのメンバ変数に組み込む,みたいな場合があるかもしれない(,むしろそれが基本的なクラス設計のやり方なのでは…?)
今回のあらすじ
- 同じモジュール内で実装したクラスでも,ポインタで持つ方が安心できる
- 面倒でもオブジェクトは
new
・delete
する癖をつけた方が良いかもしれない - そもそもそういうやり方を統一した方が混乱しない,組み込み型じゃないなら尚更
- 面倒でもオブジェクトは
- 実体で持つ場合は必ず
=
の定義を確認するべし- 例えば,「「クラス
x
」を抱えるクラスy
」をオブジェクトにする場合,クラスy
のコンストラクタが走る前にクラスx
のデフォルトコンストラクタが走る,自作クラスを実体で持つ限りこの挙動は避けられない - クラス
y
のコンストラクタ内でクラスx
のコンストラクタを叩き,その結果をメンバ変数に=
で接続すると,クラスの初期化と同様に反映できる気もするが,これは初期化の文法ではなく代入の文法として動作する - つまり演算子
=
が曖昧な自作クラスを実体としてメンバ変数に持つと予期せぬ動作が起きるかも…- 自作行列ライブラリ(公開できない)の開発中にコレに遭遇して,初めてちゃんと仕様を確認した…
- 1つ下のレイヤのオブジェクトのメンバ変数を叩いて加工するよりも,最初からそのオブジェクトに固有の演算子
=
を定義した方が,実装の責任境界が明確だし後で他のクラスに使われる時も好都合だしで生産性がある
- 例えば,「「クラス
コピー代入演算子のみを扱う,ムーブ代入演算子は扱わない ただし速度向上を考えたら後者の方が良いのかもしれない…
演算子=
のオーバーロードが必要なケース
- 他のクラスに実体で持つ予定のクラス
- そうじゃないなら要らないかも…
- 算術演算みたいな使い方をするクラス
- 行列クラスとかそもそも不可避…
- 引数の設定
- メンバ変数にポインタを持つクラス
new
・delete
またはmalloc
・free
でメモリ確保したポインタを持ってその中でデータを保持している場合,ポインタをコピーするのかポインタの中身をコピーするのかちゃんと考えて実装しよう
実装
. ├── bin │ └── main ├── libDataBase │ ├── inc │ │ └── libDataBase.hpp │ ├── lib │ │ └── liblibDataBase.a │ └── src │ ├── framework.cpp │ ├── framework.hpp │ ├── interface.cpp │ └── interface.hpp ├── makefile └── src └── main.cpp
SHELL = /bin/zsh MAIN_SRC = src/main.cpp MAIN_OBJ = main.o MAIN_BIN = bin/main GCC_BIN = clang++ GCC_FLG = -std=c++11 -O2 make : make libDataBase make main libDataBase : FORCE $(call setAllVar,libDataBase) $(GCC_BIN) $(GCC_FLG) -c $(libDataBase_SRC) @ar cr $(libDataBase_OBJ) *.o @rm -f *.o $(shell echo '#pragma once' > $(libDataBase_REP);) $(foreach hed,$(libDataBase_HED),$(shell echo '#include "$(hed)"' >> $(libDataBase_REP);)) main : FORCE $(call setRefVar,libDataBase) $(GCC_BIN) $(GCC_FLG) -c $(libDataBase_INC) $(MAIN_SRC) $(GCC_BIN) $(GCC_FLG) -o $(MAIN_BIN) $(MAIN_OBJ) $(libDataBase_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
- 適当にデータみたいなクラスを定義する
- コピー代入演算子は
void operator=(const claassName& arg)
でやった - メンバ関数に非破壊宣言の
const
をつけるときは先頭(戻り値)ではなく末尾- 戻り値に
const
をつける関数に意味があるのかは正直よくわかっていない - コピー代入演算子の引数に
const
をつけると,引数arg
を変更しないという宣言になるが,その処理でarg
の持つ関数を呼び出す場合は,それがarg
自身を変更しないことをコード上で保証する必要があるため
- 戻り値に
- 比較のためにデフォルトコンストラクタ・コピーコンストラクタ・デストラクタの全てでログを吐かせる
#pragma once #include <string> namespace database{ class framework{ private: int ID; std::string Name; int Year; public: framework(); framework(int id); framework(const framework &arg); ~framework(); void operator=(const framework &arg); void setName(std::string name); void setYear(int year); int getID() const; std::string getName() const; int getYear() const; }; }
const
に設定したgetXXX()
を使用してconst
で引数を取れるコピー代入演算子を作成する
#include <iostream> #include "framework.hpp" using namespace std; using namespace database; framework::framework(){ cout << " framework: Default Constructor, ID: " << -1 << endl; ID = -1; Name = ""; Year = 0; } framework::framework(int id){ cout << " framework: Normal Constructor, ID: " << id << endl; ID = id; Name = ""; Year = 0; } framework::framework(const framework &arg){ cout << " framework: Copy Constructor, ID: " << ID << endl; ID = arg.getID(); Name = arg.getName(); Year = arg.getYear(); } framework::~framework(){ cout << " framework: Destructor, ID: " << ID << endl; } void framework::operator=(const framework& arg){ cout << " framework: Substitute, ID: " << ID << " -> " << arg.ID << endl; ID = arg.getID(); Name = arg.getName(); Year = arg.getYear(); } void framework::setName(string name){ cout << " Set Name: " << name << endl; Name = name; } void framework::setYear(int year){ cout << " Set Year: " << year << endl; Year = year; } int framework::getID() const{ cout << " Get ID: " << ID << endl; return ID; } string framework::getName() const{ cout << " Get Name: " << Name << endl; return Name; } int framework::getYear() const{ cout << " Get Year: " << Year << endl; return Year; }
- 適当にデータベースみたいなクラスを作る
<map>
を使ってみた,あとイテレータを使用せずforeach
を使いたかったので-std=c++11
を指定したmap
自体はint
なid
とframework*
なデータを持つteamData
に使用addMember
,eraseMember
,showMember
をAPIみたいにして実装
_id_latest
は吐き出したID
の最新の値を指す,ガバガバ実装なのは許して
#pragma once #include <string> #include <map> #include "framework.hpp" namespace database{ class interface{ private: framework myData; std::map<int, framework*> teamData; int _id_latest; public: interface(); interface(std::string name, int year); ~interface(); int addMember(std::string name, int year); void eraseMember(int id); void showMember(int id); }; }
map
のデータ構造がよくわからなかったので,ポインタで持つ処理を採用した- 比較用に実体で持つオブジェクトも同時に1つ用意した,挙動を確認しよう
- デストラクタのメモリ解放処理を簡単に書きたいがためだけに
c++11
を選んだfor (const auto& member: map){}
が書けるのはc+11
以降…?auto
のところはちょっと面倒だったので厳密な書き方をしなかっただけ
#include <iostream> #include "interface.hpp" using namespace std; using namespace database; interface::interface(){ cout << "interface: Default Constructor" << endl; } interface::interface(string name, int year){ cout << "interface: Normal Constructor" << endl; myData = framework(0); myData.setName(name); myData.setYear(year); _id_latest = 0; interface::addMember(name, year); } interface::~interface(){ cout << "interface: Destructor" << endl; for (const auto& member: teamData){ delete (member.second); } teamData.clear(); } int interface::addMember(string name, int year){ cout << "Add Team Member" << endl; ++_id_latest; teamData[_id_latest] = new framework(_id_latest); teamData[_id_latest]->setName(name); teamData[_id_latest]->setYear(year); return _id_latest; } void interface::eraseMember(int id){ cout << "Erase Team Member" << endl; delete (teamData[id]); teamData.erase(id); } void interface::showMember(int id){ cout << "Show Team Member" << endl; teamData[id]->getID(); teamData[id]->getName(); teamData[id]->getYear(); }
- とても簡単な使用例
- 本当はキー待ちIOとかファイル出力とかちゃんとやるべき
不敬
#include <iostream> #include <string> #include "libDataBase.hpp" using namespace std; using namespace database; int main(){ cout << "User name and year" << endl; string name; cin >> name; int year; cin >> year; interface mydatabase = interface(name ,year); int Aiko_id = mydatabase.addMember("敬宮愛子", 2001); int Hisahito_id = mydatabase.addMember("秋篠宮悠仁", 2006); mydatabase.showMember(Aiko_id); mydatabase.showMember(Hisahito_id); mydatabase.eraseMember(Aiko_id); mydatabase.eraseMember(Hisahito_id); };
実行
% bin/main User name and year Soluna_Eureka 2000 framework: Default Constructor, ID: -1 interface: Normal Constructor framework: Normal Constructor, ID: 0 framework: Substitute, ID: -1 -> 0 Get ID: 0 Get Name: Get Year: 0 framework: Destructor, ID: 0 Set Name: Soluna_Eureka Set Year: 2000 Add Team Member framework: Normal Constructor, ID: 1 Set Name: Soluna_Eureka Set Year: 2000 Add Team Member framework: Normal Constructor, ID: 2 Set Name: 敬宮愛子 Set Year: 2001 Add Team Member framework: Normal Constructor, ID: 3 Set Name: 秋篠宮悠仁 Set Year: 2006 Show Team Member Get ID: 2 Get Name: 敬宮愛子 Get Year: 2001 Show Team Member Get ID: 3 Get Name: 秋篠宮悠仁 Get Year: 2006 Erase Team Member framework: Destructor, ID: 2 Erase Team Member framework: Destructor, ID: 3 interface: Destructor framework: Destructor, ID: 1 framework: Destructor, ID: 0
見た感じ問題なく動いてるからよし
framework: Default Constructor, ID: -1 interface: Normal Constructor framework: Normal Constructor, ID: 0 framework: Substitute, ID: -1 -> 0 Get ID: 0 Get Name: Get Year: 0 framework: Destructor, ID: 0 Set Name: Soluna_Eureka Set Year: 2000
まずframework
のデフォルトコンストラクタが起爆して,次にinterface
のコンストラクタが起爆する
そして新たにframework
コンストラクタが起爆して,生成されたオブジェクト(右辺値)のコピー代入演算子によるオブジェクト(左辺値としてのメンバ変数)への代入が発生し,最後にデストラクタでオブジェクト(右辺値)が消滅する
Add Team Member framework: Normal Constructor, ID: 1 Set Name: Soluna_Eureka Set Year: 2000 Add Team Member framework: Normal Constructor, ID: 2 Set Name: 敬宮愛子 Set Year: 2001 Add Team Member framework: Normal Constructor, ID: 3 Set Name: 秋篠宮悠仁 Set Year: 2006
ここではコンストラクタのついでにadd
を使ってmap
のnew framework
に登録している
愛子様,悠仁様,両親王陛下も.わたくしと全く同じ動作をされておりますが,全く同じ関数をご使用賜れておられるゆえ,当然のことだろうと存じ上げます(番号の順番はスルーしてくれ)
Erase Team Member framework: Destructor, ID: 2 Erase Team Member framework: Destructor, ID: 3 interface: Destructor framework: Destructor, ID: 1 framework: Destructor, ID: 0
そしてerase
もデストラクタもちゃんと動いている,撃ち漏らしはないように思える
感想
<map>
と<string>
を初めて使った,しかしmap
はともかくstring
は日本語をそのまま使えてしまっているが,文字コード的に本当にコレで良いのかどうか疑問,ぶっちゃけまだ解決しきった問題でもなさそうだけど
あとポインタで持った方がコピーコスト少ないし拡張性あるし結局はポインタを使う方が良いという結論に至った
自作の行列クラスについて
作ってみたけどコピー代入演算子の引数は参照渡しが使えなかった,だって
z = A*x + B*y
とかやる場合だと,operator*
やoperator+
の返り値は行列クラスそのものになるし,これをそのまんまoperator=
で受け取るには値渡ししかない
参照渡しと値渡しそれぞれのコピー代入演算子は,クラスで実装できてもコードで呼び出すときは曖昧さでエラーを吐くので,ユーザーは使い分けもできない
引数のconst
の厳密な管理も辛い,しかしこっちはちゃんとやるべきだった,マシなコーディング規約を考えて修正したいとは思ってる